@seamapi/react 5.0.0 → 5.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -2
- package/dist/ecosystem.js +45 -0
- package/dist/ecosystem.js.map +1 -0
- package/dist/elements.js +12872 -11737
- package/dist/elements.js.map +1 -1
- package/dist/index.css +469 -0
- package/dist/index.css.map +1 -1
- package/dist/index.min.css +1 -1
- package/dist/index.min.css.map +1 -1
- package/lib/seam/SeamProvider.d.ts +10 -3
- package/lib/seam/SeamProvider.js +5 -3
- package/lib/seam/SeamProvider.js.map +1 -1
- package/lib/seam/components/CompatibleApps/CompatibleApps.d.ts +26 -0
- package/lib/seam/components/CompatibleApps/CompatibleApps.js +126 -0
- package/lib/seam/components/CompatibleApps/CompatibleApps.js.map +1 -0
- package/lib/seam/components/SupportedDevices/DeviceModelRow.d.ts +8 -0
- package/lib/seam/components/SupportedDevices/DeviceModelRow.js +31 -0
- package/lib/seam/components/SupportedDevices/DeviceModelRow.js.map +1 -0
- package/lib/seam/components/SupportedDevices/FilterSelect.d.ts +12 -0
- package/lib/seam/components/SupportedDevices/FilterSelect.js +9 -0
- package/lib/seam/components/SupportedDevices/FilterSelect.js.map +1 -0
- package/lib/seam/components/SupportedDevices/ManufacturerSection.d.ts +10 -0
- package/lib/seam/components/SupportedDevices/ManufacturerSection.js +35 -0
- package/lib/seam/components/SupportedDevices/ManufacturerSection.js.map +1 -0
- package/lib/seam/components/SupportedDevices/SupportedDevices.d.ts +24 -0
- package/lib/seam/components/SupportedDevices/SupportedDevices.js +151 -0
- package/lib/seam/components/SupportedDevices/SupportedDevices.js.map +1 -0
- package/lib/seam/components/SupportedDevices/device-models.d.ts +47 -0
- package/lib/seam/components/SupportedDevices/device-models.js +217 -0
- package/lib/seam/components/SupportedDevices/device-models.js.map +1 -0
- package/lib/seam/components/index.d.ts +2 -0
- package/lib/seam/components/index.js +2 -0
- package/lib/seam/components/index.js.map +1 -1
- package/lib/seam/ecosystem/EcosystemProvider.d.ts +37 -0
- package/lib/seam/ecosystem/EcosystemProvider.js +56 -0
- package/lib/seam/ecosystem/EcosystemProvider.js.map +1 -0
- package/lib/seam/ecosystem/client.d.ts +28 -0
- package/lib/seam/ecosystem/client.js +74 -0
- package/lib/seam/ecosystem/client.js.map +1 -0
- package/lib/seam/ecosystem/index.d.ts +9 -0
- package/lib/seam/ecosystem/index.js +8 -0
- package/lib/seam/ecosystem/index.js.map +1 -0
- package/lib/seam/ecosystem/types.d.ts +312 -0
- package/lib/seam/ecosystem/types.js +10 -0
- package/lib/seam/ecosystem/types.js.map +1 -0
- package/lib/seam/ecosystem/use-compatible-partners.d.ts +21 -0
- package/lib/seam/ecosystem/use-compatible-partners.js +36 -0
- package/lib/seam/ecosystem/use-compatible-partners.js.map +1 -0
- package/lib/seam/ecosystem/use-ecosystem-device-model-summaries.d.ts +11 -0
- package/lib/seam/ecosystem/use-ecosystem-device-model-summaries.js +53 -0
- package/lib/seam/ecosystem/use-ecosystem-device-model-summaries.js.map +1 -0
- package/lib/seam/ecosystem/use-ecosystem-device-models.d.ts +20 -0
- package/lib/seam/ecosystem/use-ecosystem-device-models.js +51 -0
- package/lib/seam/ecosystem/use-ecosystem-device-models.js.map +1 -0
- package/lib/seam/ecosystem/use-ecosystem-manufacturers.d.ts +11 -0
- package/lib/seam/ecosystem/use-ecosystem-manufacturers.js +49 -0
- package/lib/seam/ecosystem/use-ecosystem-manufacturers.js.map +1 -0
- package/lib/seam/ecosystem/use-ecosystem-query-result.d.ts +7 -0
- package/lib/seam/ecosystem/use-ecosystem-query-result.js +7 -0
- package/lib/seam/ecosystem/use-ecosystem-query-result.js.map +1 -0
- package/lib/seam/ecosystem/use-embed-key-resolution.d.ts +20 -0
- package/lib/seam/ecosystem/use-embed-key-resolution.js +51 -0
- package/lib/seam/ecosystem/use-embed-key-resolution.js.map +1 -0
- package/lib/seam/index.d.ts +1 -0
- package/lib/seam/index.js +1 -0
- package/lib/seam/index.js.map +1 -1
- package/lib/seam/seam-components-class-name.d.ts +1 -0
- package/lib/seam/seam-components-class-name.js +2 -0
- package/lib/seam/seam-components-class-name.js.map +1 -0
- package/lib/ui/ecosystem/SearchField.d.ts +9 -0
- package/lib/ui/ecosystem/SearchField.js +8 -0
- package/lib/ui/ecosystem/SearchField.js.map +1 -0
- package/lib/ui/ecosystem/StateMessage.d.ts +8 -0
- package/lib/ui/ecosystem/StateMessage.js +5 -0
- package/lib/ui/ecosystem/StateMessage.js.map +1 -0
- package/lib/ui/ecosystem/errors.d.ts +5 -0
- package/lib/ui/ecosystem/errors.js +25 -0
- package/lib/ui/ecosystem/errors.js.map +1 -0
- package/lib/ui/ecosystem/storybook.d.ts +15 -0
- package/lib/ui/ecosystem/storybook.js +16 -0
- package/lib/ui/ecosystem/storybook.js.map +1 -0
- package/lib/ui/ecosystem/theme.d.ts +6 -0
- package/lib/ui/ecosystem/theme.js +11 -0
- package/lib/ui/ecosystem/theme.js.map +1 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +11 -3
- package/src/ecosystem.d.ts +2 -0
- package/src/ecosystem.ts +12 -0
- package/src/elements.ts +4 -0
- package/src/lib/ecosystem-element.tsx +154 -0
- package/src/lib/element.tsx +6 -0
- package/src/lib/seam/SeamProvider.tsx +18 -2
- package/src/lib/seam/components/CompatibleApps/CompatibleApps.element.ts +19 -0
- package/src/lib/seam/components/CompatibleApps/CompatibleApps.tsx +344 -0
- package/src/lib/seam/components/SupportedDevices/DeviceModelRow.tsx +108 -0
- package/src/lib/seam/components/SupportedDevices/FilterSelect.tsx +43 -0
- package/src/lib/seam/components/SupportedDevices/ManufacturerSection.tsx +114 -0
- package/src/lib/seam/components/SupportedDevices/SupportedDevices.element.ts +17 -0
- package/src/lib/seam/components/SupportedDevices/SupportedDevices.tsx +382 -0
- package/src/lib/seam/components/SupportedDevices/device-models.ts +326 -0
- package/src/lib/seam/components/ecosystem-elements.ts +2 -0
- package/src/lib/seam/components/index.ts +2 -0
- package/src/lib/seam/ecosystem/EcosystemProvider.tsx +133 -0
- package/src/lib/seam/ecosystem/client.ts +116 -0
- package/src/lib/seam/ecosystem/index.ts +19 -0
- package/src/lib/seam/ecosystem/types.ts +402 -0
- package/src/lib/seam/ecosystem/use-compatible-partners.ts +80 -0
- package/src/lib/seam/ecosystem/use-ecosystem-device-model-summaries.ts +89 -0
- package/src/lib/seam/ecosystem/use-ecosystem-device-models.ts +96 -0
- package/src/lib/seam/ecosystem/use-ecosystem-manufacturers.ts +86 -0
- package/src/lib/seam/ecosystem/use-ecosystem-query-result.ts +19 -0
- package/src/lib/seam/ecosystem/use-embed-key-resolution.ts +80 -0
- package/src/lib/seam/index.ts +1 -0
- package/src/lib/seam/seam-components-class-name.ts +1 -0
- package/src/lib/ui/ecosystem/SearchField.tsx +32 -0
- package/src/lib/ui/ecosystem/StateMessage.tsx +29 -0
- package/src/lib/ui/ecosystem/errors.ts +30 -0
- package/src/lib/ui/ecosystem/storybook.tsx +40 -0
- package/src/lib/ui/ecosystem/theme.ts +14 -0
- package/src/lib/version.ts +1 -1
- package/src/styles/_compatible-apps.scss +102 -0
- package/src/styles/_ecosystem.scss +295 -0
- package/src/styles/_main.scss +6 -0
- package/src/styles/_supported-devices.scss +153 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type EcosystemApiError,
|
|
5
|
+
fetchEcosystem,
|
|
6
|
+
} from 'lib/seam/ecosystem/client.js'
|
|
7
|
+
import { useEcosystemContext } from 'lib/seam/ecosystem/EcosystemProvider.js'
|
|
8
|
+
import type {
|
|
9
|
+
CompatibilityResponse,
|
|
10
|
+
CompatiblePartner,
|
|
11
|
+
} from 'lib/seam/ecosystem/types.js'
|
|
12
|
+
import {
|
|
13
|
+
retryEcosystemQuery,
|
|
14
|
+
type UseEcosystemQueryResult,
|
|
15
|
+
} from 'lib/seam/ecosystem/use-ecosystem-query-result.js'
|
|
16
|
+
|
|
17
|
+
export interface UseCompatiblePartnersParams {
|
|
18
|
+
/** Device category, e.g., `smartlock`, `thermostat`, `sensor`, `intercom`. */
|
|
19
|
+
deviceType: string
|
|
20
|
+
/** DeviceDB manufacturer slug, e.g., `schlage`. */
|
|
21
|
+
manufacturer?: string | undefined
|
|
22
|
+
/** DeviceDB provider slug, e.g., `salto-ks`. */
|
|
23
|
+
provider?: string | undefined
|
|
24
|
+
/** DeviceDB capability, e.g., `can_provision_mobile_keys`. */
|
|
25
|
+
capability?: string | undefined
|
|
26
|
+
/** Industry slug, e.g., `short-term-rental`. */
|
|
27
|
+
industry?: string | undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type UseCompatiblePartnersData = CompatiblePartner[]
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The live partners that support a device category, manufacturer, provider
|
|
34
|
+
* or capability through Seam, from Ecosystem `GET /v1/compatibility`.
|
|
35
|
+
* Declared-first: a partner is listed only where Seam or the partner declared support.
|
|
36
|
+
*/
|
|
37
|
+
export function useCompatiblePartners({
|
|
38
|
+
deviceType,
|
|
39
|
+
manufacturer = '',
|
|
40
|
+
provider = '',
|
|
41
|
+
capability = '',
|
|
42
|
+
industry = '',
|
|
43
|
+
}: UseCompatiblePartnersParams): UseEcosystemQueryResult<
|
|
44
|
+
'partners',
|
|
45
|
+
UseCompatiblePartnersData
|
|
46
|
+
> {
|
|
47
|
+
const { embedKey, endpoint } = useEcosystemContext()
|
|
48
|
+
|
|
49
|
+
const params = {
|
|
50
|
+
device_category: deviceType.trim(),
|
|
51
|
+
manufacturer_slug: manufacturer.trim(),
|
|
52
|
+
provider_slug: provider.trim(),
|
|
53
|
+
capability: capability.trim(),
|
|
54
|
+
industry: industry.trim(),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { data, ...rest } = useQuery<
|
|
58
|
+
UseCompatiblePartnersData,
|
|
59
|
+
EcosystemApiError
|
|
60
|
+
>({
|
|
61
|
+
enabled: params.device_category.length > 0,
|
|
62
|
+
queryKey: [
|
|
63
|
+
'ecosystem',
|
|
64
|
+
endpoint,
|
|
65
|
+
embedKey ?? null,
|
|
66
|
+
'compatibility',
|
|
67
|
+
params,
|
|
68
|
+
],
|
|
69
|
+
queryFn: async ({ signal }) => {
|
|
70
|
+
const { partners } = await fetchEcosystem<CompatibilityResponse>(
|
|
71
|
+
'compatibility',
|
|
72
|
+
{ endpoint, embedKey, params, signal }
|
|
73
|
+
)
|
|
74
|
+
return partners
|
|
75
|
+
},
|
|
76
|
+
retry: retryEcosystemQuery,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return { ...rest, partners: data }
|
|
80
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type EcosystemApiError,
|
|
5
|
+
fetchEcosystem,
|
|
6
|
+
} from 'lib/seam/ecosystem/client.js'
|
|
7
|
+
import { useEcosystemContext } from 'lib/seam/ecosystem/EcosystemProvider.js'
|
|
8
|
+
import type {
|
|
9
|
+
DeviceModelSummariesListResponse,
|
|
10
|
+
DeviceModelSummary,
|
|
11
|
+
} from 'lib/seam/ecosystem/types.js'
|
|
12
|
+
import type { UseEcosystemDeviceModelsParams } from 'lib/seam/ecosystem/use-ecosystem-device-models.js'
|
|
13
|
+
import {
|
|
14
|
+
retryEcosystemQuery,
|
|
15
|
+
type UseEcosystemQueryResult,
|
|
16
|
+
} from 'lib/seam/ecosystem/use-ecosystem-query-result.js'
|
|
17
|
+
|
|
18
|
+
export type UseEcosystemDeviceModelSummariesParams =
|
|
19
|
+
UseEcosystemDeviceModelsParams
|
|
20
|
+
|
|
21
|
+
export type UseEcosystemDeviceModelSummariesData = DeviceModelSummary[]
|
|
22
|
+
|
|
23
|
+
const pageSize = 500
|
|
24
|
+
const maxPages = 20
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Published device models in the slim summary shape from Ecosystem
|
|
28
|
+
* `GET /v1/device_models/summaries/list` (no commerce data),
|
|
29
|
+
* following pagination to return every matching model.
|
|
30
|
+
*/
|
|
31
|
+
export function useEcosystemDeviceModelSummaries({
|
|
32
|
+
manufacturer = '',
|
|
33
|
+
deviceType = '',
|
|
34
|
+
provider = '',
|
|
35
|
+
capability = '',
|
|
36
|
+
enabled = true,
|
|
37
|
+
}: UseEcosystemDeviceModelSummariesParams = {}): UseEcosystemQueryResult<
|
|
38
|
+
'deviceModels',
|
|
39
|
+
UseEcosystemDeviceModelSummariesData
|
|
40
|
+
> {
|
|
41
|
+
const { embedKey, endpoint } = useEcosystemContext()
|
|
42
|
+
|
|
43
|
+
const params = {
|
|
44
|
+
manufacturer: manufacturer.trim(),
|
|
45
|
+
device_type: deviceType.trim(),
|
|
46
|
+
provider: provider.trim(),
|
|
47
|
+
capability: capability.trim(),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { data, ...rest } = useQuery<
|
|
51
|
+
UseEcosystemDeviceModelSummariesData,
|
|
52
|
+
EcosystemApiError
|
|
53
|
+
>({
|
|
54
|
+
enabled,
|
|
55
|
+
queryKey: [
|
|
56
|
+
'ecosystem',
|
|
57
|
+
endpoint,
|
|
58
|
+
embedKey ?? null,
|
|
59
|
+
'device_models',
|
|
60
|
+
'summaries',
|
|
61
|
+
'list',
|
|
62
|
+
params,
|
|
63
|
+
],
|
|
64
|
+
queryFn: async ({ signal }) => {
|
|
65
|
+
const deviceModels: DeviceModelSummary[] = []
|
|
66
|
+
let pageCursor: string | undefined
|
|
67
|
+
for (let page = 0; page < maxPages; page++) {
|
|
68
|
+
const res = await fetchEcosystem<DeviceModelSummariesListResponse>(
|
|
69
|
+
'device_models/summaries/list',
|
|
70
|
+
{
|
|
71
|
+
endpoint,
|
|
72
|
+
embedKey,
|
|
73
|
+
params: { ...params, limit: pageSize, page_cursor: pageCursor },
|
|
74
|
+
signal,
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
deviceModels.push(...res.device_models)
|
|
78
|
+
const { has_next_page: hasNextPage, next_page_cursor: nextPageCursor } =
|
|
79
|
+
res.pagination
|
|
80
|
+
if (!hasNextPage || nextPageCursor == null) break
|
|
81
|
+
pageCursor = nextPageCursor
|
|
82
|
+
}
|
|
83
|
+
return deviceModels
|
|
84
|
+
},
|
|
85
|
+
retry: retryEcosystemQuery,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return { ...rest, deviceModels: data }
|
|
89
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type EcosystemApiError,
|
|
5
|
+
fetchEcosystem,
|
|
6
|
+
} from 'lib/seam/ecosystem/client.js'
|
|
7
|
+
import { useEcosystemContext } from 'lib/seam/ecosystem/EcosystemProvider.js'
|
|
8
|
+
import type {
|
|
9
|
+
DeviceModel,
|
|
10
|
+
DeviceModelsListResponse,
|
|
11
|
+
} from 'lib/seam/ecosystem/types.js'
|
|
12
|
+
import {
|
|
13
|
+
retryEcosystemQuery,
|
|
14
|
+
type UseEcosystemQueryResult,
|
|
15
|
+
} from 'lib/seam/ecosystem/use-ecosystem-query-result.js'
|
|
16
|
+
|
|
17
|
+
export interface UseEcosystemDeviceModelsParams {
|
|
18
|
+
/** DeviceDB manufacturer slug, e.g., `schlage`. */
|
|
19
|
+
manufacturer?: string | undefined
|
|
20
|
+
/** DeviceDB device type slug, e.g., `smartlock`. */
|
|
21
|
+
deviceType?: string | undefined
|
|
22
|
+
/** DeviceDB provider slug, e.g., `salto-ks`. */
|
|
23
|
+
provider?: string | undefined
|
|
24
|
+
/** Only device models with a computed capability of this name. */
|
|
25
|
+
capability?: string | undefined
|
|
26
|
+
/** Set to false to wait before fetching. */
|
|
27
|
+
enabled?: boolean | undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type UseEcosystemDeviceModelsData = DeviceModel[]
|
|
31
|
+
|
|
32
|
+
const pageSize = 500
|
|
33
|
+
const maxPages = 20
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Published device models from Ecosystem `GET /v1/device_models/list`,
|
|
37
|
+
* following pagination to return every matching model.
|
|
38
|
+
*/
|
|
39
|
+
export function useEcosystemDeviceModels({
|
|
40
|
+
manufacturer = '',
|
|
41
|
+
deviceType = '',
|
|
42
|
+
provider = '',
|
|
43
|
+
capability = '',
|
|
44
|
+
enabled = true,
|
|
45
|
+
}: UseEcosystemDeviceModelsParams = {}): UseEcosystemQueryResult<
|
|
46
|
+
'deviceModels',
|
|
47
|
+
UseEcosystemDeviceModelsData
|
|
48
|
+
> {
|
|
49
|
+
const { embedKey, endpoint } = useEcosystemContext()
|
|
50
|
+
|
|
51
|
+
const params = {
|
|
52
|
+
manufacturer: manufacturer.trim(),
|
|
53
|
+
device_type: deviceType.trim(),
|
|
54
|
+
provider: provider.trim(),
|
|
55
|
+
capability: capability.trim(),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { data, ...rest } = useQuery<
|
|
59
|
+
UseEcosystemDeviceModelsData,
|
|
60
|
+
EcosystemApiError
|
|
61
|
+
>({
|
|
62
|
+
enabled,
|
|
63
|
+
queryKey: [
|
|
64
|
+
'ecosystem',
|
|
65
|
+
endpoint,
|
|
66
|
+
embedKey ?? null,
|
|
67
|
+
'device_models',
|
|
68
|
+
'list',
|
|
69
|
+
params,
|
|
70
|
+
],
|
|
71
|
+
queryFn: async ({ signal }) => {
|
|
72
|
+
const deviceModels: DeviceModel[] = []
|
|
73
|
+
let pageCursor: string | undefined
|
|
74
|
+
for (let page = 0; page < maxPages; page++) {
|
|
75
|
+
const res = await fetchEcosystem<DeviceModelsListResponse>(
|
|
76
|
+
'device_models/list',
|
|
77
|
+
{
|
|
78
|
+
endpoint,
|
|
79
|
+
embedKey,
|
|
80
|
+
params: { ...params, limit: pageSize, page_cursor: pageCursor },
|
|
81
|
+
signal,
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
deviceModels.push(...res.device_models)
|
|
85
|
+
const { has_next_page: hasNextPage, next_page_cursor: nextPageCursor } =
|
|
86
|
+
res.pagination
|
|
87
|
+
if (!hasNextPage || nextPageCursor == null) break
|
|
88
|
+
pageCursor = nextPageCursor
|
|
89
|
+
}
|
|
90
|
+
return deviceModels
|
|
91
|
+
},
|
|
92
|
+
retry: retryEcosystemQuery,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return { ...rest, deviceModels: data }
|
|
96
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type EcosystemApiError,
|
|
5
|
+
fetchEcosystem,
|
|
6
|
+
} from 'lib/seam/ecosystem/client.js'
|
|
7
|
+
import { useEcosystemContext } from 'lib/seam/ecosystem/EcosystemProvider.js'
|
|
8
|
+
import type {
|
|
9
|
+
Manufacturer,
|
|
10
|
+
ManufacturersListResponse,
|
|
11
|
+
} from 'lib/seam/ecosystem/types.js'
|
|
12
|
+
import {
|
|
13
|
+
retryEcosystemQuery,
|
|
14
|
+
type UseEcosystemQueryResult,
|
|
15
|
+
} from 'lib/seam/ecosystem/use-ecosystem-query-result.js'
|
|
16
|
+
|
|
17
|
+
export interface UseEcosystemManufacturersParams {
|
|
18
|
+
/** Only manufacturers with these slugs. */
|
|
19
|
+
slugs?: string[] | undefined
|
|
20
|
+
/** Set to false to wait before fetching. */
|
|
21
|
+
enabled?: boolean | undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type UseEcosystemManufacturersData = Manufacturer[]
|
|
25
|
+
|
|
26
|
+
const pageSize = 500
|
|
27
|
+
const maxPages = 10
|
|
28
|
+
|
|
29
|
+
/** Published manufacturers from Ecosystem `GET /v1/manufacturers/list`. */
|
|
30
|
+
export function useEcosystemManufacturers({
|
|
31
|
+
slugs,
|
|
32
|
+
enabled = true,
|
|
33
|
+
}: UseEcosystemManufacturersParams = {}): UseEcosystemQueryResult<
|
|
34
|
+
'manufacturers',
|
|
35
|
+
UseEcosystemManufacturersData
|
|
36
|
+
> {
|
|
37
|
+
const { embedKey, endpoint } = useEcosystemContext()
|
|
38
|
+
|
|
39
|
+
const sortedSlugs =
|
|
40
|
+
slugs == null
|
|
41
|
+
? null
|
|
42
|
+
: [...new Set(slugs)].sort((a, b) => a.localeCompare(b))
|
|
43
|
+
|
|
44
|
+
const { data, ...rest } = useQuery<
|
|
45
|
+
UseEcosystemManufacturersData,
|
|
46
|
+
EcosystemApiError
|
|
47
|
+
>({
|
|
48
|
+
enabled: enabled && (sortedSlugs == null || sortedSlugs.length > 0),
|
|
49
|
+
queryKey: [
|
|
50
|
+
'ecosystem',
|
|
51
|
+
endpoint,
|
|
52
|
+
embedKey ?? null,
|
|
53
|
+
'manufacturers',
|
|
54
|
+
'list',
|
|
55
|
+
{ slugs: sortedSlugs },
|
|
56
|
+
],
|
|
57
|
+
queryFn: async ({ signal }) => {
|
|
58
|
+
const manufacturers: Manufacturer[] = []
|
|
59
|
+
let pageCursor: string | undefined
|
|
60
|
+
for (let page = 0; page < maxPages; page++) {
|
|
61
|
+
const res = await fetchEcosystem<ManufacturersListResponse>(
|
|
62
|
+
'manufacturers/list',
|
|
63
|
+
{
|
|
64
|
+
endpoint,
|
|
65
|
+
embedKey,
|
|
66
|
+
params: {
|
|
67
|
+
slugs: sortedSlugs ?? undefined,
|
|
68
|
+
limit: pageSize,
|
|
69
|
+
page_cursor: pageCursor,
|
|
70
|
+
},
|
|
71
|
+
signal,
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
manufacturers.push(...res.manufacturers)
|
|
75
|
+
const { has_next_page: hasNextPage, next_page_cursor: nextPageCursor } =
|
|
76
|
+
res.pagination
|
|
77
|
+
if (!hasNextPage || nextPageCursor == null) break
|
|
78
|
+
pageCursor = nextPageCursor
|
|
79
|
+
}
|
|
80
|
+
return manufacturers
|
|
81
|
+
},
|
|
82
|
+
retry: retryEcosystemQuery,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
return { ...rest, manufacturers: data }
|
|
86
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { UseQueryResult } from '@tanstack/react-query'
|
|
2
|
+
|
|
3
|
+
import type { EcosystemApiError } from 'lib/seam/ecosystem/client.js'
|
|
4
|
+
|
|
5
|
+
export type UseEcosystemQueryResult<Field extends string, Data> = Omit<
|
|
6
|
+
UseQueryResult<Data, EcosystemApiError>,
|
|
7
|
+
'data'
|
|
8
|
+
> & {
|
|
9
|
+
[key in Field]?: Data
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Retry server and network errors, but not client errors such as an invalid embed key. */
|
|
13
|
+
export const retryEcosystemQuery = (
|
|
14
|
+
failureCount: number,
|
|
15
|
+
error: EcosystemApiError
|
|
16
|
+
): boolean => {
|
|
17
|
+
if (error.status >= 400 && error.status < 500) return false
|
|
18
|
+
return failureCount < 2
|
|
19
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type EcosystemApiError,
|
|
5
|
+
fetchEcosystem,
|
|
6
|
+
} from 'lib/seam/ecosystem/client.js'
|
|
7
|
+
import { useEcosystemContext } from 'lib/seam/ecosystem/EcosystemProvider.js'
|
|
8
|
+
import type { EmbedResolution } from 'lib/seam/ecosystem/types.js'
|
|
9
|
+
import {
|
|
10
|
+
retryEcosystemQuery,
|
|
11
|
+
type UseEcosystemQueryResult,
|
|
12
|
+
} from 'lib/seam/ecosystem/use-ecosystem-query-result.js'
|
|
13
|
+
|
|
14
|
+
export interface UseEmbedKeyResolutionParams {
|
|
15
|
+
/** Resolve the partner's capability exclusions for this capability. */
|
|
16
|
+
capability?: string | undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type UseEmbedKeyResolutionData = EmbedResolution
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validates the provider's embed key against the page origin with
|
|
23
|
+
* Ecosystem `GET /v1/embed/resolve` and returns the key's public record
|
|
24
|
+
* and, for partner keys, what the partner's widgets must not show.
|
|
25
|
+
* Disabled when there is no embed key.
|
|
26
|
+
*/
|
|
27
|
+
export function useEmbedKeyResolution({
|
|
28
|
+
capability = '',
|
|
29
|
+
}: UseEmbedKeyResolutionParams = {}): UseEcosystemQueryResult<
|
|
30
|
+
'embedResolution',
|
|
31
|
+
UseEmbedKeyResolutionData
|
|
32
|
+
> {
|
|
33
|
+
const { embedKey, endpoint } = useEcosystemContext()
|
|
34
|
+
|
|
35
|
+
const params = { capability: capability.trim() }
|
|
36
|
+
|
|
37
|
+
const { data, ...rest } = useQuery<
|
|
38
|
+
UseEmbedKeyResolutionData,
|
|
39
|
+
EcosystemApiError
|
|
40
|
+
>({
|
|
41
|
+
enabled: embedKey != null,
|
|
42
|
+
queryKey: [
|
|
43
|
+
'ecosystem',
|
|
44
|
+
endpoint,
|
|
45
|
+
embedKey ?? null,
|
|
46
|
+
'embed',
|
|
47
|
+
'resolve',
|
|
48
|
+
params,
|
|
49
|
+
],
|
|
50
|
+
queryFn: async ({ signal }) =>
|
|
51
|
+
await fetchEcosystem<EmbedResolution>('embed/resolve', {
|
|
52
|
+
endpoint,
|
|
53
|
+
embedKey,
|
|
54
|
+
params,
|
|
55
|
+
signal,
|
|
56
|
+
}),
|
|
57
|
+
retry: retryEcosystemQuery,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
return { ...rest, embedResolution: data }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The manufacturer an embed key is scoped to, if any.
|
|
65
|
+
* Prefers the `manufacturer_context` Ecosystem returns for manufacturer keys (DEVDB-439),
|
|
66
|
+
* and falls back to the key's own `manufacturer_slug`.
|
|
67
|
+
*/
|
|
68
|
+
export const getEmbedKeyManufacturer = (
|
|
69
|
+
resolution: EmbedResolution | null | undefined
|
|
70
|
+
): string | null => {
|
|
71
|
+
if (resolution == null) return null
|
|
72
|
+
const contextSlug = resolution.manufacturer_context?.manufacturer_slug
|
|
73
|
+
if (contextSlug != null && contextSlug.length > 0) return contextSlug
|
|
74
|
+
const { owner_kind: ownerKind, manufacturer_slug: slug } =
|
|
75
|
+
resolution.embed_key
|
|
76
|
+
if (ownerKind === 'manufacturer' && slug != null && slug.length > 0) {
|
|
77
|
+
return slug
|
|
78
|
+
}
|
|
79
|
+
return null
|
|
80
|
+
}
|
package/src/lib/seam/index.ts
CHANGED
|
@@ -10,5 +10,6 @@ export * from './connected-accounts/use-connected-account.js'
|
|
|
10
10
|
export * from './devices/use-device.js'
|
|
11
11
|
export * from './devices/use-device-providers.js'
|
|
12
12
|
export * from './devices/use-devices.js'
|
|
13
|
+
export * from './ecosystem/index.js'
|
|
13
14
|
export * from './SeamProvider.js'
|
|
14
15
|
export * from './use-seam-query-result.js'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const seamComponentsClassName = 'seam-components'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SearchIcon } from 'lib/icons/Search.js'
|
|
2
|
+
|
|
3
|
+
interface SearchFieldProps {
|
|
4
|
+
value: string
|
|
5
|
+
onChange: (value: string) => void
|
|
6
|
+
label: string
|
|
7
|
+
disabled?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function SearchField({
|
|
11
|
+
value,
|
|
12
|
+
onChange,
|
|
13
|
+
label,
|
|
14
|
+
disabled = false,
|
|
15
|
+
}: SearchFieldProps): JSX.Element {
|
|
16
|
+
return (
|
|
17
|
+
<label className='seam-ecosystem-search'>
|
|
18
|
+
<SearchIcon aria-hidden className='seam-ecosystem-search-icon' />
|
|
19
|
+
<input
|
|
20
|
+
type='search'
|
|
21
|
+
className='seam-ecosystem-search-input'
|
|
22
|
+
placeholder={label}
|
|
23
|
+
aria-label={label}
|
|
24
|
+
value={value}
|
|
25
|
+
disabled={disabled}
|
|
26
|
+
onChange={(event) => {
|
|
27
|
+
onChange(event.target.value)
|
|
28
|
+
}}
|
|
29
|
+
/>
|
|
30
|
+
</label>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PropsWithChildren } from 'react'
|
|
2
|
+
|
|
3
|
+
interface StateMessageProps extends PropsWithChildren {
|
|
4
|
+
actionLabel?: string
|
|
5
|
+
onAction?: () => void
|
|
6
|
+
role?: 'status' | 'alert'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function StateMessage({
|
|
10
|
+
children,
|
|
11
|
+
actionLabel,
|
|
12
|
+
onAction,
|
|
13
|
+
role = 'status',
|
|
14
|
+
}: StateMessageProps): JSX.Element {
|
|
15
|
+
return (
|
|
16
|
+
<div className='seam-ecosystem-message' role={role}>
|
|
17
|
+
<p>{children}</p>
|
|
18
|
+
{actionLabel != null && onAction != null && (
|
|
19
|
+
<button
|
|
20
|
+
type='button'
|
|
21
|
+
className='seam-ecosystem-button'
|
|
22
|
+
onClick={onAction}
|
|
23
|
+
>
|
|
24
|
+
{actionLabel}
|
|
25
|
+
</button>
|
|
26
|
+
)}
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { EcosystemApiError } from 'lib/seam/ecosystem/client.js'
|
|
2
|
+
|
|
3
|
+
/** A message for an Ecosystem error, and whether retrying could help. */
|
|
4
|
+
export const getEcosystemErrorMessage = (
|
|
5
|
+
error: unknown,
|
|
6
|
+
fallback: string
|
|
7
|
+
): { message: string; canRetry: boolean } => {
|
|
8
|
+
if (error instanceof EcosystemApiError) {
|
|
9
|
+
if (error.type === 'embed_key_required') {
|
|
10
|
+
return { message: t.embedKeyRequired, canRetry: false }
|
|
11
|
+
}
|
|
12
|
+
if (error.type === 'origin_not_allowed') {
|
|
13
|
+
return { message: t.originNotAllowed, canRetry: false }
|
|
14
|
+
}
|
|
15
|
+
if (error.status === 401) {
|
|
16
|
+
return { message: t.invalidEmbedKey, canRetry: false }
|
|
17
|
+
}
|
|
18
|
+
if (error.status === 403) {
|
|
19
|
+
return { message: t.originNotAllowed, canRetry: false }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { message: fallback, canRetry: true }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const t = {
|
|
26
|
+
embedKeyRequired:
|
|
27
|
+
'An embed key is required to show this list on this website.',
|
|
28
|
+
invalidEmbedKey: 'The embed key is invalid or has been revoked.',
|
|
29
|
+
originNotAllowed: 'The embed key is not allowed on this website.',
|
|
30
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Decorator } from '@storybook/react'
|
|
2
|
+
|
|
3
|
+
import { EcosystemProvider } from 'lib/seam/ecosystem/EcosystemProvider.js'
|
|
4
|
+
|
|
5
|
+
// Embed keys accepted by the fake Ecosystem API served at /api/ecosystem
|
|
6
|
+
// (.storybook/fake-ecosystem.js). The Storybook never uses a real key or production data.
|
|
7
|
+
export const storybookEmbedKeys = {
|
|
8
|
+
seam: 'eco_live_storybook',
|
|
9
|
+
partner: 'eco_live_storybook_partner',
|
|
10
|
+
manufacturer: 'eco_live_storybook_schlage',
|
|
11
|
+
wrongOrigin: 'eco_live_storybook_wrong_origin',
|
|
12
|
+
invalid: 'eco_live_invalid',
|
|
13
|
+
} as const
|
|
14
|
+
|
|
15
|
+
export const storybookEcosystemEndpoint = '/api/ecosystem/v1/'
|
|
16
|
+
|
|
17
|
+
export interface EcosystemStoryArgs {
|
|
18
|
+
embedKey?: string | undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Wraps a story in an EcosystemProvider pointed at the fake Ecosystem API. */
|
|
22
|
+
export const withFakeEcosystem: Decorator<EcosystemStoryArgs> = (
|
|
23
|
+
Story,
|
|
24
|
+
context
|
|
25
|
+
) => (
|
|
26
|
+
<EcosystemProvider
|
|
27
|
+
embedKey={context.args.embedKey}
|
|
28
|
+
ecosystemEndpoint={storybookEcosystemEndpoint}
|
|
29
|
+
disableCssInjection
|
|
30
|
+
disableFontInjection
|
|
31
|
+
>
|
|
32
|
+
<Story />
|
|
33
|
+
</EcosystemProvider>
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
export const withDarkBackground: Decorator = (Story) => (
|
|
37
|
+
<div style={{ background: '#0b0d10', padding: 24, borderRadius: 12 }}>
|
|
38
|
+
<Story />
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type EcosystemTheme = 'light' | 'dark' | 'auto'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The class name for an Ecosystem component theme.
|
|
5
|
+
* `auto` follows the visitor's `prefers-color-scheme`.
|
|
6
|
+
*/
|
|
7
|
+
export const getEcosystemThemeClassName = (
|
|
8
|
+
theme: string | undefined
|
|
9
|
+
): string => {
|
|
10
|
+
if (theme === 'dark' || theme === 'auto') {
|
|
11
|
+
return `seam-ecosystem-theme-${theme}`
|
|
12
|
+
}
|
|
13
|
+
return 'seam-ecosystem-theme-light'
|
|
14
|
+
}
|
package/src/lib/version.ts
CHANGED