diff --git a/examples/supported-devices-prototypes/src/ProviderRequirements.tsx b/examples/supported-devices-prototypes/src/ProviderRequirements.tsx index 52e3f66e6..604e498dd 100644 --- a/examples/supported-devices-prototypes/src/ProviderRequirements.tsx +++ b/examples/supported-devices-prototypes/src/ProviderRequirements.tsx @@ -1,7 +1,7 @@ import { RequirementIcon } from 'lib/seam/components/SupportedDevices/RequirementIcon.js' import { - getRequirementKind, + getRequirementKinds, type RequirementKind, type ReviewDevice, } from './model.js' @@ -12,6 +12,10 @@ const columns: Array<{ kind: RequirementKind; label: string }> = [ { kind: 'bridge', label: 'Bridge' }, { kind: 'keypad', label: 'Keypad' }, ] +const additionalColumns: typeof columns = [ + { kind: 'legal', label: 'Legal' }, + { kind: 'other', label: 'Other' }, +] export function ProviderRequirements({ provider, @@ -20,11 +24,14 @@ export function ProviderRequirements({ provider: ReviewDevice['providers'][number] view: 'matrix' | 'list' }): JSX.Element { - const visibleColumns = provider.requirements.some( - ({ message }) => getRequirementKind(message) === 'other' - ) - ? [...columns, { kind: 'other' as const, label: 'Other' }] - : columns + const visibleColumns = [ + ...columns, + ...additionalColumns.filter(({ kind }) => + provider.requirements.some(({ message }) => + getRequirementKinds(message).includes(kind) + ) + ), + ] return ( <> {provider.capabilities.length === 0 ? ( @@ -42,7 +49,7 @@ export function ProviderRequirements({ > 3 ? '' : undefined} + data-extra-columns={visibleColumns.length - columns.length} > {visibleColumns.map(({ kind, label }) => { - const required = capability.messages.some( - (message) => getRequirementKind(message) === kind + const required = capability.messages.some((message) => + getRequirementKinds(message).includes(kind) ) return (
Supported capabilities and their additional requirements @@ -63,8 +70,8 @@ export function ProviderRequirements({
{capability.label} @@ -127,8 +134,8 @@ export function ProviderRequirements({
{visibleColumns .filter(({ kind }) => - capability.messages.some( - (message) => getRequirementKind(message) === kind + capability.messages.some((message) => + getRequirementKinds(message).includes(kind) ) ) .map(({ kind, label }) => ( diff --git a/examples/supported-devices-prototypes/src/model.test.ts b/examples/supported-devices-prototypes/src/model.test.ts new file mode 100644 index 000000000..0b31ae2f1 --- /dev/null +++ b/examples/supported-devices-prototypes/src/model.test.ts @@ -0,0 +1,43 @@ +import { expect, test } from 'vitest' + +import { getRequirementKinds, getRequirementPills } from './model.js' + +const legal = + 'To be able to use this system, you will need to first sign an EULA with Salto.' +const combined = + 'To be able to use this system, you will both need to first sign an EULA with Salto and install a Seam bridge to make it cloud-ready.' + +test('an EULA is a legal prerequisite, distinct from a subscription', () => { + expect(getRequirementKinds(legal)).toEqual(['legal']) + expect(getRequirementKinds('Sign an end-user license agreement.')).toEqual([ + 'legal', + ]) + expect(getRequirementKinds('Sign a waiver.')).toEqual(['legal']) +}) + +test('a combined provider requirement preserves legal and bridge categories and source copy', () => { + expect(getRequirementKinds(combined)).toEqual(['legal', 'bridge']) + expect( + getRequirementPills({ + requirements: [{ message: combined, capabilities: ['Access codes'] }], + }) + ).toEqual([ + { label: 'Accessories Required', messages: [combined] }, + { label: 'Legal Requirements', messages: [combined] }, + ]) +}) + +test('existing accessory and subscription categories remain distinct; unknown prose stays explicit', () => { + expect(getRequirementKinds('A compatible bridge is required.')).toEqual([ + 'bridge', + ]) + expect(getRequirementKinds('An external keypad is required.')).toEqual([ + 'keypad', + ]) + expect( + getRequirementKinds('A paying Nuki SmartHosting subscription is required.') + ).toEqual(['subscription']) + expect( + getRequirementKinds('Contact the provider to configure access.') + ).toEqual(['other']) +}) diff --git a/examples/supported-devices-prototypes/src/model.ts b/examples/supported-devices-prototypes/src/model.ts index a191466c8..4d2077d66 100644 --- a/examples/supported-devices-prototypes/src/model.ts +++ b/examples/supported-devices-prototypes/src/model.ts @@ -37,15 +37,28 @@ export interface VariantProps { requirementView?: 'matrix' | 'list' } -export type RequirementKind = 'subscription' | 'bridge' | 'keypad' | 'other' +export type RequirementKind = + | 'subscription' + | 'bridge' + | 'keypad' + | 'legal' + | 'other' -export function getRequirementKind(message: string): RequirementKind { +/** UI aliases for source prose, which can describe more than one prerequisite. */ +export function getRequirementKinds(message: string): RequirementKind[] { const known: Record = { 'A compatible bridge is required.': 'bridge', 'An external keypad is required.': 'keypad', 'A paying Nuki SmartHosting subscription is required.': 'subscription', } - return known[message] ?? 'other' + const kinds: RequirementKind[] = [] + const knownKind = known[message.trim()] + if (knownKind != null) kinds.push(knownKind) + if (/\b(?:eula|waiver|end[- ]user licen[cs]e agreement)\b/i.test(message)) + kinds.push('legal') + if (/\binstall\s+(?:a\s+)?seam\s+bridge\b/i.test(message)) + kinds.push('bridge') + return kinds.length === 0 ? ['other'] : [...new Set(kinds)] } export function reviewDevice( @@ -116,22 +129,26 @@ function reviewRequirements( /** Presentation aliases for known requirements; unfamiliar source copy stays explicit. */ export function getRequirementPills( - device: ReviewDevice + device: Pick ): Array<{ label: string; messages: string[] }> { const labels: Record = { bridge: 'Accessories Required', keypad: 'Accessories Required', subscription: 'Subscription Required', + legal: 'Legal Requirements', other: 'Other Requirements', } const groups = new Map() for (const { message } of device.requirements) { - const label = labels[getRequirementKind(message)] - groups.set(label, [...(groups.get(label) ?? []), message]) + for (const kind of getRequirementKinds(message)) { + const label = labels[kind] + groups.set(label, [...(groups.get(label) ?? []), message]) + } } return [ 'Accessories Required', 'Subscription Required', + 'Legal Requirements', 'Other Requirements', ].flatMap((label) => { const messages = groups.get(label) diff --git a/examples/supported-devices-prototypes/src/style.css b/examples/supported-devices-prototypes/src/style.css index 58172b0ac..00581baa7 100644 --- a/examples/supported-devices-prototypes/src/style.css +++ b/examples/supported-devices-prototypes/src/style.css @@ -839,9 +839,12 @@ body { border-collapse: collapse; font-size: 12px; } -.seam-proto-requirement-matrix[data-has-other] { +.seam-proto-requirement-matrix[data-extra-columns='1'] { min-width: 356px; } +.seam-proto-requirement-matrix[data-extra-columns='2'] { + min-width: 420px; +} .seam-proto-requirement-matrix th, .seam-proto-requirement-matrix td { padding: 10px 4px; diff --git a/src/lib/seam/components/SupportedDevices/RequirementIcon.tsx b/src/lib/seam/components/SupportedDevices/RequirementIcon.tsx index 66f210f2d..454d32ad5 100644 --- a/src/lib/seam/components/SupportedDevices/RequirementIcon.tsx +++ b/src/lib/seam/components/SupportedDevices/RequirementIcon.tsx @@ -13,7 +13,14 @@ export function RequirementIcon({ message }: { message: string }): JSX.Element { strokeLinecap='round' strokeLinejoin='round' > - {/subscription|licen[sc]e|paying|paid|fees?/i.test(message) ? ( + {/\b(?:legal|eula|waiver|end[- ]user licen[cs]e agreement)\b/i.test( + message + ) ? ( + <> + + + + ) : /subscription|licen[sc]e|paying|paid|fees?/i.test(message) ? ( <>