From f4bd58fb2b92022eca92f2247731f62733bc3293 Mon Sep 17 00:00:00 2001 From: Sy Bohy Date: Fri, 30 Oct 2026 09:15:31 -0700 Subject: [PATCH] fix: distinguish unsupported device providers (DEVDB-475) --- .../src/ProviderRequirements.tsx | 13 ++ .../src/model.test.ts | 152 +++++++++++++++++- .../supported-devices-prototypes/src/model.ts | 42 ++++- .../src/shared.tsx | 9 +- .../src/style.css | 10 ++ src/lib/seam/ecosystem/types.ts | 5 +- 6 files changed, 224 insertions(+), 7 deletions(-) diff --git a/examples/supported-devices-prototypes/src/ProviderRequirements.tsx b/examples/supported-devices-prototypes/src/ProviderRequirements.tsx index 604e498dd..bfa1358c1 100644 --- a/examples/supported-devices-prototypes/src/ProviderRequirements.tsx +++ b/examples/supported-devices-prototypes/src/ProviderRequirements.tsx @@ -24,6 +24,19 @@ export function ProviderRequirements({ provider: ReviewDevice['providers'][number] view: 'matrix' | 'list' }): JSX.Element { + if (provider.integrationStatus === 'unsupported') + return ( + + Request Integration → + + ) + const visibleColumns = [ ...columns, ...additionalColumns.filter(({ kind }) => diff --git a/examples/supported-devices-prototypes/src/model.test.ts b/examples/supported-devices-prototypes/src/model.test.ts index 0b31ae2f1..11ec50fd8 100644 --- a/examples/supported-devices-prototypes/src/model.test.ts +++ b/examples/supported-devices-prototypes/src/model.test.ts @@ -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.' @@ -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 +): 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([ + '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(['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') +}) diff --git a/examples/supported-devices-prototypes/src/model.ts b/examples/supported-devices-prototypes/src/model.ts index 4d2077d66..2b3ef5f5d 100644 --- a/examples/supported-devices-prototypes/src/model.ts +++ b/examples/supported-devices-prototypes/src/model.ts @@ -7,6 +7,7 @@ import { import type { DeviceAsset, DeviceModelSummary, + IntegrationSupportLevel, ProviderRef, ProviderRequirements, } from 'lib/seam/ecosystem/types.js' @@ -28,6 +29,7 @@ export interface ReviewDevice { interface ReviewProvider extends ProviderRef { logos?: DeviceAsset[] primary: boolean + integrationStatus: 'supported' | 'unsupported' | 'unknown' capabilities: Capability[] requirements: ReviewDevice['requirements'] } @@ -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 @@ -101,6 +125,16 @@ export function reviewDevice( } } +function getIntegrationStatus( + levels: Array +): 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') diff --git a/examples/supported-devices-prototypes/src/shared.tsx b/examples/supported-devices-prototypes/src/shared.tsx index 0c26563fa..34bfd726e 100644 --- a/examples/supported-devices-prototypes/src/shared.tsx +++ b/examples/supported-devices-prototypes/src/shared.tsx @@ -258,7 +258,11 @@ function ExpandedDetails({

Available providers

    {device.providers.map((provider) => ( -
  • +
  • {provider.display_name ?? provider.slug} {provider.primary && This guide} + {provider.integrationStatus === 'unsupported' && ( + Not supported + )}
    } export type CapabilityName =