Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ export function ProviderRequirements({
provider: ReviewDevice['providers'][number]
view: 'matrix' | 'list'
}): JSX.Element {
if (provider.integrationStatus === 'unsupported')
return (
<a
className='seam-proto-request-integration'
href='https://www.seam.co/request-new-integration'
target='_blank'
rel='noopener noreferrer'
aria-label={`Request Integration with ${provider.display_name ?? provider.slug} (opens in a new tab)`}
>
Request Integration <span aria-hidden>→</span>
</a>
)

const visibleColumns = [
...columns,
...additionalColumns.filter(({ kind }) =>
Expand Down
152 changes: 151 additions & 1 deletion examples/supported-devices-prototypes/src/model.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { expect, test } from 'vitest'

import { getRequirementKinds, getRequirementPills } from './model.js'
import type {
CapabilitySummary,
DeviceModelSummary,
IntegrationSupportLevel,
ProviderRef,
ProviderRequirements,
} from 'lib/seam/ecosystem/types.js'

import {
getRequirementKinds,
getRequirementPills,
reviewDevice,
} from './model.js'

const legal =
'To be able to use this system, you will need to first sign an EULA with Salto.'
Expand Down Expand Up @@ -41,3 +53,141 @@ test('existing accessory and subscription categories remain distinct; unknown pr
getRequirementKinds('Contact the provider to configure access.')
).toEqual(['other'])
})

const providerRef = (slug: string): ProviderRef => ({
provider_id: slug,
slug,
display_name: slug,
})

function deviceWithProviders(
entries: Array<{ slug: string; capabilities: CapabilitySummary[] }>
): DeviceModelSummary {
return {
device_model_id: 'lock',
slug: 'lock',
display_name: 'Lock',
description_md: null,
product_url: null,
no_longer_sold: false,
is_forbidden_to_3dparty: false,
manufacturer: null,
device_type: null,
device_type_properties: null,
featured_image: null,
variants: [],
providers: entries.map(({ slug }) => providerRef(slug)),
capabilities_by_provider: entries.map(({ slug, capabilities }) => ({
provider: providerRef(slug),
capabilities,
})),
capabilities_stale: false,
updated_at: '2026-10-01T00:00:00.000Z',
}
}

const capability = (
level: IntegrationSupportLevel | null,
name: CapabilitySummary['name'] = 'can_remotely_unlock'
): CapabilitySummary => ({
name,
supported: 'yes',
integration_support_level: level,
report: null,
})

const metadata = (
slug: string,
levels: Array<IntegrationSupportLevel | null>
): ProviderRequirements => ({
...providerRef(slug),
has_requirements: false,
requirements_md: null,
integrations: levels.map((level) => ({ integration_support_level: level })),
})

test('unsupported providers sort last, even when they are the guide provider', () => {
const model = deviceWithProviders([
{ slug: 'homelok', capabilities: [capability('inquire')] },
{ slug: 'space', capabilities: [capability('stable')] },
{ slug: 'ks', capabilities: [capability('stable')] },
])
const result = reviewDevice(model)
expect(result.providers.map((provider) => provider.slug)).toEqual([
'ks',
'space',
'homelok',
])
expect(
result.providers.map((provider) => provider.integrationStatus)
).toEqual(['supported', 'supported', 'unsupported'])
expect(result.providers[2]?.capabilities).toEqual([])
})

test('the supported guide provider precedes other supported providers', () => {
const model = deviceWithProviders([
{ slug: 'ks', capabilities: [capability('stable')] },
{ slug: 'homelok', capabilities: [capability('inquire')] },
{ slug: 'another-live-provider', capabilities: [capability('beta')] },
])
expect(
reviewDevice(model).providers.map((provider) => provider.slug)
).toEqual(['ks', 'another-live-provider', 'homelok'])
})

test.each<IntegrationSupportLevel>([
'inquire',
'unavailable',
'planned',
'in-progress',
])('a provider with only %s integrations is not supported', (level) => {
const model = deviceWithProviders([{ slug: 'cloud', capabilities: [] }])
expect(
reviewDevice(model, [metadata('cloud', [level])]).providers[0]
?.integrationStatus
).toBe('unsupported')
})

test.each<IntegrationSupportLevel>(['stable', 'beta'])(
'a %s provider stays supported despite missing or stale device capabilities',
(level) => {
const model = deviceWithProviders([{ slug: 'cloud', capabilities: [] }])
model.capabilities_stale = true
const result = reviewDevice(model, [metadata('cloud', ['inquire', level])])
expect(result.providers[0]?.integrationStatus).toBe('supported')
expect(result.providers[0]?.capabilities).toEqual([])
}
)

test('missing support levels and connectivity alone do not imply an unsupported integration', () => {
const model = deviceWithProviders([
{ slug: 'empty', capabilities: [] },
{
slug: 'connectivity-only',
capabilities: [capability(null, 'device_connectivity')],
},
{ slug: 'null-level', capabilities: [capability(null)] },
])
expect(
reviewDevice(model).providers.every(
(provider) => provider.integrationStatus === 'unknown'
)
).toBe(true)
expect(
reviewDevice(model, [metadata('empty', ['inquire', null])]).providers.find(
(provider) => provider.slug === 'empty'
)?.integrationStatus
).toBe('unknown')
})

test('stale device reports cannot classify a provider without metadata', () => {
const model = deviceWithProviders([
{ slug: 'cloud', capabilities: [capability('inquire')] },
])
model.capabilities_stale = true
expect(reviewDevice(model).providers[0]?.integrationStatus).toBe('unknown')
expect(
reviewDevice(model, [metadata('cloud', ['inquire'])]).providers[0]
?.integrationStatus
).toBe('unsupported')
})
42 changes: 38 additions & 4 deletions examples/supported-devices-prototypes/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import type {
DeviceAsset,
DeviceModelSummary,
IntegrationSupportLevel,
ProviderRef,
ProviderRequirements,
} from 'lib/seam/ecosystem/types.js'
Expand All @@ -28,6 +29,7 @@ export interface ReviewDevice {
interface ReviewProvider extends ProviderRef {
logos?: DeviceAsset[]
primary: boolean
integrationStatus: 'supported' | 'unsupported' | 'unknown'
capabilities: Capability[]
requirements: ReviewDevice['requirements']
}
Expand Down Expand Up @@ -76,23 +78,45 @@ export function reviewDevice(
providers: allProvidersSource.providers
.map((provider) => {
const details = providers.find((entry) => entry.slug === provider.slug)
const providerCapabilities =
allProvidersSource.capabilities_by_provider.filter(
(entry) => entry.provider.slug === provider.slug
)
const capabilities = reviewCapabilities({
...allProvidersSource,
capabilities_by_provider:
allProvidersSource.capabilities_by_provider.filter(
(entry) => entry.provider.slug === provider.slug
),
capabilities_by_provider: providerCapabilities,
})
// Provider metadata distinguishes a missing integration from missing
// device capabilities. Connectivity alone does not imply Seam support.
const levels =
details?.integrations != null && details.integrations.length > 0
? details.integrations.map(
(entry) => entry.integration_support_level
)
: allProvidersSource.capabilities_stale
? []
: providerCapabilities.flatMap((entry) =>
entry.capabilities
.filter(
(capability) => capability.name !== 'device_connectivity'
)
.map((capability) => capability.integration_support_level)
)
return {
...provider,
display_name: details?.display_name ?? provider.display_name,
logos: details?.logos,
primary: provider.slug === primarySlug,
integrationStatus: getIntegrationStatus(levels),
capabilities,
requirements: reviewRequirements(capabilities),
}
})
.sort((a, b) => {
const unsupported =
Number(a.integrationStatus === 'unsupported') -
Number(b.integrationStatus === 'unsupported')
if (unsupported !== 0) return unsupported
if (a.primary !== b.primary) return a.primary ? -1 : 1
return (a.display_name ?? a.slug).localeCompare(
b.display_name ?? b.slug
Expand All @@ -101,6 +125,16 @@ export function reviewDevice(
}
}

function getIntegrationStatus(
levels: Array<IntegrationSupportLevel | null>
): ReviewProvider['integrationStatus'] {
if (levels.some((level) => level === 'stable' || level === 'beta'))
return 'supported'
if (levels.length === 0 || levels.some((level) => level == null))
return 'unknown'
return 'unsupported'
}

function reviewCapabilities(source: DeviceModelSummary): Capability[] {
return getSupportedCapabilities(source)
.filter((capability) => capability.name !== 'device_connectivity')
Expand Down
9 changes: 8 additions & 1 deletion examples/supported-devices-prototypes/src/shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,11 @@ function ExpandedDetails({
<h3>Available providers</h3>
<ul className='seam-proto-provider-list'>
{device.providers.map((provider) => (
<li className='seam-proto-provider' key={provider.provider_id}>
<li
className='seam-proto-provider'
key={provider.provider_id}
data-integration-status={provider.integrationStatus}
>
<div className='seam-compact-detail-heading'>
<BrandMark
className='seam-compact-provider-mark'
Expand All @@ -268,6 +272,9 @@ function ExpandedDetails({
<div className='seam-proto-provider-name'>
<strong>{provider.display_name ?? provider.slug}</strong>
{provider.primary && <span>This guide</span>}
{provider.integrationStatus === 'unsupported' && (
<span>Not supported</span>
)}
</div>
</div>
<ProviderRequirements
Expand Down
10 changes: 10 additions & 0 deletions examples/supported-devices-prototypes/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,16 @@ body {
color: var(--seam-ecosystem-fg-2);
font-size: 10px;
}
.seam-proto-request-integration {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 44px;
margin-top: 4px;
color: var(--seam-ecosystem-accent);
font-size: 12px;
text-underline-offset: 3px;
}
.seam-proto-matrix-scroll {
max-width: min(100%, 440px);
overflow-x: auto;
Expand Down
5 changes: 4 additions & 1 deletion src/lib/seam/ecosystem/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,14 @@ export interface ProviderRef {
display_name: string | null
}

/** Fields used from public `providers/list` for branding and legacy report text. */
/** Fields used from public `providers/list` for branding, support, and report text. */
export interface ProviderRequirements extends ProviderRef {
has_requirements: boolean | null
requirements_md: string | null
logos?: DeviceAsset[]
integrations?: Array<{
integration_support_level: IntegrationSupportLevel | null
}>
}

export type CapabilityName =
Expand Down
Loading