diff --git a/README.md b/README.md index 1b5a9d68b..30daff5f5 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,24 @@ $ npm install @seamapi/react [npm]: https://www.npmjs.com/ +### Peer dependencies + +If your project uses a recent version of npm, peer dependencies will be handled automatically. + + +If you package manager does not automatically install peer dependencies, install these packages: + + +``` +@seamapi/http @seamapi/types +``` + +If using TypeScript, install these packages as development dependencies: + +``` +@seamapi/types @types/react-dom +``` + ## Usage ### With React diff --git a/package-lock.json b/package-lock.json index 6af15a715..3542caeb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "MIT", "dependencies": { "@floating-ui/react": "^0.27.5", - "@seamapi/http": "^1.30.2", "@tanstack/react-query": "^5.27.5", "classnames": "^2.3.2", "luxon": "^3.3.0", @@ -27,6 +26,7 @@ "@rxfork/r2wc-react-to-web-component": "^2.4.0", "@seamapi/fake-devicedb": "^1.6.1", "@seamapi/fake-seam-connect": "^1.76.0", + "@seamapi/http": "^1.38.3", "@seamapi/types": "^1.395.3", "@storybook/addon-designs": "^7.0.1", "@storybook/addon-essentials": "^7.0.2", @@ -84,6 +84,7 @@ "npm": ">= 9.0.0" }, "peerDependencies": { + "@seamapi/http": "^1.37.0", "@seamapi/types": "^1.395.3", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", @@ -5872,22 +5873,22 @@ } }, "node_modules/@seamapi/http": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/@seamapi/http/-/http-1.30.2.tgz", - "integrity": "sha512-0kb1Y/d5ifXxQLLLMZ072wQCc23mmpXlpVquK3B3asFCvphCCXEE4ksFDDETEWgY+wWwPrq51GAqvHRs3rhGGg==", + "version": "1.38.3", + "resolved": "https://registry.npmjs.org/@seamapi/http/-/http-1.38.3.tgz", + "integrity": "sha512-KBUZpSCGs2UpnzP5480Qa4W8IioCKH0fssrTt7DyMGJ/T9o1mWuxgvqGf6l89ISNo2Z0H6I27Q/K9dWzg7sqYw==", + "dev": true, "license": "MIT", "dependencies": { - "@seamapi/url-search-params-serializer": "^1.2.0", - "axios": "^1.5.0", - "axios-better-stacktrace": "^2.1.7", + "@seamapi/url-search-params-serializer": "^2.0.0-beta.2", + "axios": "^1.9.0", "axios-retry": "^4.4.2" }, "engines": { - "node": ">=18.12.0", - "npm": ">= 9.0.0" + "node": ">=20.9.0", + "npm": ">=10.1.0" }, "peerDependencies": { - "@seamapi/types": "^1.384.0" + "@seamapi/types": "^1.420.2" }, "peerDependenciesMeta": { "@seamapi/types": { @@ -5896,10 +5897,10 @@ } }, "node_modules/@seamapi/types": { - "version": "1.395.3", - "resolved": "https://registry.npmjs.org/@seamapi/types/-/types-1.395.3.tgz", - "integrity": "sha512-HfqkuV/au/9V/XoBNZCv2sx6QuRdms68bC60/W23AdZ19h2p7fOHaUkc4DKJy8lvzTdr/499B8D6VCdRaTZSKw==", - "devOptional": true, + "version": "1.424.0", + "resolved": "https://registry.npmjs.org/@seamapi/types/-/types-1.424.0.tgz", + "integrity": "sha512-neg4BsO0Aw+DWknPOS4v75Mee++il+3xMFVB8F2kOx8BY4hjTTZGgFD6nyb6kNDtrHhu3L7TiXfp50yPqCO2xw==", + "dev": true, "license": "MIT", "engines": { "node": ">=18.12.0", @@ -5910,9 +5911,10 @@ } }, "node_modules/@seamapi/url-search-params-serializer": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@seamapi/url-search-params-serializer/-/url-search-params-serializer-1.3.0.tgz", - "integrity": "sha512-SyS2ioYQx/WlOvcWK1le7iCmGGIIFiLPg5edXyOYEFPiItZVYvQpy1PjafTD1WNFrEaHShbQQo33IllYr7kOeg==", + "version": "2.0.0-beta.2", + "resolved": "https://registry.npmjs.org/@seamapi/url-search-params-serializer/-/url-search-params-serializer-2.0.0-beta.2.tgz", + "integrity": "sha512-ASHYo5/0IY7iB/cWcZA9b6meAa5b22NfEZyIuZQ2I90L7WeKzeWxEmusA4nfc26giQOeuEF1xAIGSdGrT+lGZg==", + "dev": true, "license": "MIT", "engines": { "node": ">=18.12.0", @@ -11078,6 +11080,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, "license": "MIT" }, "node_modules/available-typed-arrays": { @@ -11097,9 +11100,10 @@ } }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "dev": true, "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -11107,19 +11111,11 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/axios-better-stacktrace": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/axios-better-stacktrace/-/axios-better-stacktrace-2.1.7.tgz", - "integrity": "sha512-m16wNbfb7crBpENBukoBdN1G9NwqSCkuIeKjSEP2iUoFvgNUnSW1/1Ov79EkTu29xmg+TsngJcy2lfwqBzVT7g==", - "license": "MIT", - "peerDependencies": { - "axios": "*" - } - }, "node_modules/axios-retry": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "dev": true, "license": "Apache-2.0", "dependencies": { "is-retry-allowed": "^2.2.0" @@ -12226,6 +12222,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -13228,6 +13225,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -16230,6 +16228,7 @@ "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, "funding": [ { "type": "individual", @@ -16428,6 +16427,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -18085,6 +18085,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -19574,6 +19575,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -19583,6 +19585,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -21240,6 +21243,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, "license": "MIT" }, "node_modules/prr": { @@ -27535,7 +27539,7 @@ "version": "3.24.4", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", - "devOptional": true, + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index d1e1dc443..0e8813f70 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "npm": ">= 9.0.0" }, "peerDependencies": { + "@seamapi/http": "^1.37.0", "@seamapi/types": "^1.395.3", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", @@ -128,7 +129,6 @@ }, "dependencies": { "@floating-ui/react": "^0.27.5", - "@seamapi/http": "^1.30.2", "@tanstack/react-query": "^5.27.5", "classnames": "^2.3.2", "luxon": "^3.3.0", @@ -145,6 +145,7 @@ "@rxfork/r2wc-react-to-web-component": "^2.4.0", "@seamapi/fake-devicedb": "^1.6.1", "@seamapi/fake-seam-connect": "^1.76.0", + "@seamapi/http": "^1.38.3", "@seamapi/types": "^1.395.3", "@storybook/addon-designs": "^7.0.1", "@storybook/addon-essentials": "^7.0.2", diff --git a/src/lib/index.ts b/src/lib/index.ts index 4842d5b89..71a7d02f1 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,3 +1,4 @@ export * from './seam/components/index.js' export * from './seam/index.js' export * from './seam/SeamProvider.js' +export * from './seam/SeamQueryProvider.js' diff --git a/src/lib/seam/SeamProvider.tsx b/src/lib/seam/SeamProvider.tsx index 8c60fe9c5..22a4ffee9 100644 --- a/src/lib/seam/SeamProvider.tsx +++ b/src/lib/seam/SeamProvider.tsx @@ -1,16 +1,13 @@ -import type { - SeamHttp, - SeamHttpOptionsWithClientSessionToken, -} from '@seamapi/http/connect' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { - createContext, - type PropsWithChildren, - useContext, - useEffect, - useMemo, -} from 'react' +import type { SeamHttp } from '@seamapi/http/connect' +import type { QueryClient } from '@tanstack/react-query' +import { createContext, type PropsWithChildren, useMemo } from 'react' +import { + SeamQueryProvider, + type SeamQueryProviderPropsWithClient, + type SeamQueryProviderPropsWithClientSessionToken, + type SeamQueryProviderPropsWithPublishableKey, +} from 'lib/seam/SeamQueryProvider.js' import { useSeamFont } from 'lib/seam/use-seam-font.js' import { useSeamStyles } from 'lib/seam/use-seam-styles.js' import { @@ -19,8 +16,6 @@ import { useUserTelemetry, } from 'lib/telemetry/index.js' -import { useSeamClient } from './use-seam-client.js' - declare global { // eslint-disable-next-line no-var var seam: SeamProviderProps | undefined @@ -30,33 +25,28 @@ declare global { var seamTelemetryClient: TelemetryClient | undefined } -export interface SeamContext { - client: SeamHttp | null - clientOptions?: SeamProviderClientOptions | undefined - publishableKey?: string | undefined - userIdentifierKey?: string | undefined - clientSessionToken?: string | undefined -} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SeamContext {} export type SeamProviderProps = | SeamProviderPropsWithClient | SeamProviderPropsWithPublishableKey | SeamProviderPropsWithClientSessionToken -export interface SeamProviderPropsWithClient extends SeamProviderBaseProps { - client: SeamHttp -} +export interface SeamProviderPropsWithClient + extends SeamQueryProviderPropsWithClient, + SeamProviderBaseProps {} export interface SeamProviderPropsWithPublishableKey extends SeamProviderBaseProps, - SeamProviderClientOptions { + SeamQueryProviderPropsWithPublishableKey { publishableKey: string userIdentifierKey?: string } export interface SeamProviderPropsWithClientSessionToken extends SeamProviderBaseProps, - SeamProviderClientOptions { + SeamQueryProviderPropsWithClientSessionToken { clientSessionToken: string } @@ -70,12 +60,6 @@ interface SeamProviderBaseProps extends PropsWithChildren { onSessionUpdate?: (client: SeamHttp) => void } -type SeamClientOptions = SeamHttpOptionsWithClientSessionToken - -export type SeamProviderClientOptions = Pick - -const defaultQueryClient = new QueryClient() - export const seamComponentsClassName = 'seam-components' export function SeamProvider({ @@ -84,40 +68,20 @@ export function SeamProvider({ disableCssInjection = false, disableFontInjection = false, unminifiyCss = false, - onSessionUpdate = () => {}, - queryClient, telemetryClient, ...props }: SeamProviderProps): JSX.Element { useSeamStyles({ disabled: disableCssInjection, unminified: unminifiyCss }) useSeamFont({ disabled: disableFontInjection }) + const { Provider } = seamContext + + const endpoint = 'endpoint' in props ? props.endpoint : undefined const value = useMemo(() => { const context = createSeamContextValue(props) - if ( - context.client == null && - context.publishableKey == null && - context.clientSessionToken == null - ) { - return defaultSeamContextValue - } return context }, [props]) - if ( - value.client == null && - value.publishableKey == null && - value.clientSessionToken == null - ) { - throw new Error( - `Must provide either a Seam client, clientSessionToken, or a publishableKey.` - ) - } - - const { Provider } = seamContext - - const endpoint = 'endpoint' in props ? props.endpoint : undefined - return (
- + - {children} + {children} - +
) } -function Wrapper({ - onSessionUpdate, - children, -}: Required> & - PropsWithChildren): JSX.Element | null { +function Telemetry({ children }: PropsWithChildren): JSX.Element | null { useUserTelemetry() - - const { client } = useSeamClient() - useEffect(() => { - if (client != null) onSessionUpdate(client) - }, [onSessionUpdate, client]) - return <>{children} } const createDefaultSeamContextValue = (): SeamContext => { try { if (globalThis.seam == null) { - return { client: null } + return {} } return createSeamContextValue(globalThis.seam) } catch (err) { // eslint-disable-next-line no-console console.warn(err) - return { client: null } + return {} } } -const createSeamContextValue = (options: SeamProviderProps): SeamContext => { - if (isSeamProviderPropsWithClient(options)) { - return options - } - - if (isSeamProviderPropsWithClientSessionToken(options)) { - const { clientSessionToken, ...clientOptions } = options - return { - clientSessionToken, - clientOptions, - client: null, - } - } - - if (isSeamProviderPropsWithPublishableKey(options)) { - const { publishableKey, userIdentifierKey, ...clientOptions } = options - return { - publishableKey, - userIdentifierKey, - clientOptions, - client: null, - } - } - - return { client: null } +const createSeamContextValue = (_options: SeamProviderProps): SeamContext => { + return {} } const defaultSeamContextValue = createDefaultSeamContextValue() -export const seamContext = createContext(defaultSeamContextValue) - -export function useSeamContext(): SeamContext { - return useContext(seamContext) -} - -const isSeamProviderPropsWithClient = ( - props: SeamProviderProps -): props is SeamProviderPropsWithClient => { - if (!('client' in props)) return false - - const { client, ...otherProps } = props - if (client == null) return false - - const otherNonNullProps = Object.values(otherProps).filter((v) => v != null) - if (otherNonNullProps.length > 0) { - throw new InvalidSeamProviderProps( - `The client prop cannot be used with ${otherNonNullProps.join(' or ')}.` - ) - } - - return true -} - -const isSeamProviderPropsWithPublishableKey = ( - props: SeamProviderProps -): props is SeamProviderPropsWithPublishableKey & SeamProviderClientOptions => { - if (!('publishableKey' in props)) return false - - const { publishableKey } = props - if (publishableKey == null) return false - - if ('client' in props && props.client != null) { - throw new InvalidSeamProviderProps( - 'The client prop cannot be used with the publishableKey prop.' - ) - } - - if ('clientSessionToken' in props && props.clientSessionToken != null) { - throw new InvalidSeamProviderProps( - 'The clientSessionToken prop cannot be used with the publishableKey prop.' - ) - } - - return true -} - -const isSeamProviderPropsWithClientSessionToken = ( - props: SeamProviderProps -): props is SeamProviderPropsWithClientSessionToken & - SeamProviderClientOptions => { - if (!('clientSessionToken' in props)) return false - - const { clientSessionToken } = props - if (clientSessionToken == null) return false - - if ('client' in props && props.client != null) { - throw new InvalidSeamProviderProps( - 'The client prop cannot be used with the clientSessionToken prop.' - ) - } - - if ('publishableKey' in props && props.publishableKey != null) { - throw new InvalidSeamProviderProps( - 'The publishableKey prop cannot be used with the clientSessionToken prop.' - ) - } - - if ('userIdentifierKey' in props && props.userIdentifierKey != null) { - throw new InvalidSeamProviderProps( - 'The userIdentifierKey prop cannot be used with the clientSessionToken prop.' - ) - } - - return true -} - -class InvalidSeamProviderProps extends Error { - constructor(message: string) { - super(`SeamProvider received invalid props: ${message}`) - this.name = this.constructor.name - } -} +const seamContext = createContext(defaultSeamContextValue) diff --git a/src/lib/seam/SeamQueryProvider.tsx b/src/lib/seam/SeamQueryProvider.tsx new file mode 100644 index 000000000..41759ba68 --- /dev/null +++ b/src/lib/seam/SeamQueryProvider.tsx @@ -0,0 +1,248 @@ +import type { + SeamHttp, + SeamHttpEndpoints, + SeamHttpOptionsWithClientSessionToken, +} from '@seamapi/http/connect' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { + createContext, + type PropsWithChildren, + useContext, + useEffect, + useMemo, +} from 'react' + +import { useSeamClient } from './use-seam-client.js' + +export interface SeamQueryContext { + client: SeamHttp | null + endpointClient: SeamHttpEndpoints | null + clientOptions?: SeamQueryProviderClientOptions | undefined + publishableKey?: string | undefined + userIdentifierKey?: string | undefined + clientSessionToken?: string | undefined +} + +export type SeamQueryProviderProps = + | SeamQueryProviderPropsWithClient + | SeamQueryProviderPropsWithPublishableKey + | SeamQueryProviderPropsWithClientSessionToken + +export interface SeamQueryProviderPropsWithClient + extends SeamQueryProviderBaseProps { + client: SeamHttp +} + +export interface SeamQueryProviderPropsWithPublishableKey + extends SeamQueryProviderBaseProps, + SeamQueryProviderClientOptions { + publishableKey: string + userIdentifierKey?: string +} + +export interface SeamQueryProviderPropsWithClientSessionToken + extends SeamQueryProviderBaseProps, + SeamQueryProviderClientOptions { + clientSessionToken: string +} + +interface SeamQueryProviderBaseProps extends PropsWithChildren { + queryClient?: QueryClient | undefined + onSessionUpdate?: (client: SeamHttp) => void +} + +type SeamClientOptions = SeamHttpOptionsWithClientSessionToken + +export type SeamQueryProviderClientOptions = Pick + +const defaultQueryClient = new QueryClient() + +export function SeamQueryProvider({ + children, + onSessionUpdate = () => {}, + queryClient, + ...props +}: SeamQueryProviderProps): JSX.Element { + const value = useMemo(() => { + const context = createSeamQueryContextValue(props) + if ( + context.client == null && + context.publishableKey == null && + context.clientSessionToken == null + ) { + return defaultSeamQueryContextValue + } + return context + }, [props]) + + if ( + value.client == null && + value.publishableKey == null && + value.clientSessionToken == null + ) { + throw new Error( + `Must provide either a Seam client, clientSessionToken, or a publishableKey.` + ) + } + + const { Provider } = seamContext + + return ( + + + {children} + + + ) +} + +function Session({ + onSessionUpdate, + children, +}: Required> & + PropsWithChildren): JSX.Element | null { + const { client } = useSeamClient() + useEffect(() => { + if (client != null) onSessionUpdate(client) + }, [onSessionUpdate, client]) + + return <>{children} +} + +const createDefaultSeamQueryContextValue = (): SeamQueryContext => { + try { + if (globalThis.seam == null) { + return { client: null, endpointClient: null } + } + return createSeamQueryContextValue(globalThis.seam) + } catch (err) { + // eslint-disable-next-line no-console + console.warn(err) + return { client: null, endpointClient: null } + } +} + +const createSeamQueryContextValue = ( + options: SeamQueryProviderProps +): SeamQueryContext => { + if (isSeamQueryProviderPropsWithClient(options)) { + return { + ...options, + endpointClient: null, + } + } + + if (isSeamQueryProviderPropsWithClientSessionToken(options)) { + const { clientSessionToken, ...clientOptions } = options + return { + clientSessionToken, + clientOptions, + client: null, + endpointClient: null, + } + } + + if (isSeamQueryProviderPropsWithPublishableKey(options)) { + const { publishableKey, userIdentifierKey, ...clientOptions } = options + return { + publishableKey, + userIdentifierKey, + clientOptions, + client: null, + endpointClient: null, + } + } + + return { client: null, endpointClient: null } +} + +const defaultSeamQueryContextValue = createDefaultSeamQueryContextValue() + +export const seamContext = createContext( + defaultSeamQueryContextValue +) + +export function useSeamQueryContext(): SeamQueryContext { + return useContext(seamContext) +} + +const isSeamQueryProviderPropsWithClient = ( + props: SeamQueryProviderProps +): props is SeamQueryProviderPropsWithClient => { + if (!('client' in props)) return false + + const { client, ...otherProps } = props + if (client == null) return false + + const otherNonNullProps = Object.values(otherProps).filter((v) => v != null) + if (otherNonNullProps.length > 0) { + throw new InvalidSeamQueryProviderProps( + `The client prop cannot be used with ${otherNonNullProps.join(' or ')}.` + ) + } + + return true +} + +const isSeamQueryProviderPropsWithPublishableKey = ( + props: SeamQueryProviderProps +): props is SeamQueryProviderPropsWithPublishableKey & + SeamQueryProviderClientOptions => { + if (!('publishableKey' in props)) return false + + const { publishableKey } = props + if (publishableKey == null) return false + + if ('client' in props && props.client != null) { + throw new InvalidSeamQueryProviderProps( + 'The client prop cannot be used with the publishableKey prop.' + ) + } + + if ('clientSessionToken' in props && props.clientSessionToken != null) { + throw new InvalidSeamQueryProviderProps( + 'The clientSessionToken prop cannot be used with the publishableKey prop.' + ) + } + + return true +} + +const isSeamQueryProviderPropsWithClientSessionToken = ( + props: SeamQueryProviderProps +): props is SeamQueryProviderPropsWithClientSessionToken & + SeamQueryProviderClientOptions => { + if (!('clientSessionToken' in props)) return false + + const { clientSessionToken } = props + if (clientSessionToken == null) return false + + if ('client' in props && props.client != null) { + throw new InvalidSeamQueryProviderProps( + 'The client prop cannot be used with the clientSessionToken prop.' + ) + } + + if ('publishableKey' in props && props.publishableKey != null) { + throw new InvalidSeamQueryProviderProps( + 'The publishableKey prop cannot be used with the clientSessionToken prop.' + ) + } + + if ('userIdentifierKey' in props && props.userIdentifierKey != null) { + throw new InvalidSeamQueryProviderProps( + 'The userIdentifierKey prop cannot be used with the clientSessionToken prop.' + ) + } + + return true +} + +class InvalidSeamQueryProviderProps extends Error { + constructor(message: string) { + super(`SeamQueryProvider received invalid props: ${message}`) + this.name = this.constructor.name + } +} diff --git a/src/lib/seam/access-codes/use-access-code.ts b/src/lib/seam/access-codes/use-access-code.ts index c07047d94..b1ee269ce 100644 --- a/src/lib/seam/access-codes/use-access-code.ts +++ b/src/lib/seam/access-codes/use-access-code.ts @@ -6,7 +6,7 @@ import type { AccessCode } from '@seamapi/types/connect' import { useQuery } from '@tanstack/react-query' import { useSeamClient } from 'lib/seam/use-seam-client.js' -import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js' +import type { UseSeamQueryResultLegacy } from 'lib/seam/use-seam-query-result.js' export type UseAccessCodeParams = AccessCodesGetParams @@ -14,7 +14,7 @@ export type UseAccessCodeData = AccessCode | null export function useAccessCode( params: UseAccessCodeParams -): UseSeamQueryResult<'accessCode', UseAccessCodeData> { +): UseSeamQueryResultLegacy<'accessCode', UseAccessCodeData> { const { client } = useSeamClient() const { data, ...rest } = useQuery({ enabled: client != null, diff --git a/src/lib/seam/access-codes/use-access-codes.ts b/src/lib/seam/access-codes/use-access-codes.ts index 18789ceac..8eafc2d6b 100644 --- a/src/lib/seam/access-codes/use-access-codes.ts +++ b/src/lib/seam/access-codes/use-access-codes.ts @@ -6,7 +6,7 @@ import type { AccessCode } from '@seamapi/types/connect' import { useQuery, useQueryClient } from '@tanstack/react-query' import { useSeamClient } from 'lib/seam/use-seam-client.js' -import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js' +import type { UseSeamQueryResultLegacy } from 'lib/seam/use-seam-query-result.js' export type UseAccessCodesParams = AccessCodesListParams @@ -14,7 +14,7 @@ export type UseAccessCodesData = AccessCode[] export function useAccessCodes( params: UseAccessCodesParams -): UseSeamQueryResult<'accessCodes', UseAccessCodesData> { +): UseSeamQueryResultLegacy<'accessCodes', UseAccessCodesData> { const { client } = useSeamClient() const queryClient = useQueryClient() diff --git a/src/lib/seam/access-codes/use-create-access-code.ts b/src/lib/seam/access-codes/use-create-access-code.ts index a7a80886e..e79d944ea 100644 --- a/src/lib/seam/access-codes/use-create-access-code.ts +++ b/src/lib/seam/access-codes/use-create-access-code.ts @@ -1,39 +1,14 @@ -import type { - AccessCodesCreateBody, - SeamHttpApiError, -} from '@seamapi/http/connect' -import type { AccessCode } from '@seamapi/types/connect' -import { - useMutation, - type UseMutationResult, - useQueryClient, -} from '@tanstack/react-query' - -import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js' - -export type UseCreateAccessCodeParams = never +import { useQueryClient } from '@tanstack/react-query' -export type UseCreateAccessCodeData = AccessCode - -export type UseCreateAccessCodeMutationVariables = AccessCodesCreateBody +import { + useSeamMutation, + type UseSeamMutationResult, +} from '../use-seam-mutation.js' -export function useCreateAccessCode(): UseMutationResult< - UseCreateAccessCodeData, - SeamHttpApiError, - UseCreateAccessCodeMutationVariables -> { - const { client } = useSeamClient() +export function useCreateAccessCode(): UseSeamMutationResult<'/access_codes/create'> { const queryClient = useQueryClient() - return useMutation< - UseCreateAccessCodeData, - SeamHttpApiError, - UseCreateAccessCodeMutationVariables - >({ - mutationFn: async (variables) => { - if (client === null) throw new NullSeamClientError() - return await client.accessCodes.create(variables) - }, + return useSeamMutation('/access_codes/create', { onSuccess: (data) => { queryClient.setQueryData( ['access_codes', 'get', { access_code_id: data.access_code_id }], diff --git a/src/lib/seam/access-codes/use-generate-access-code-code.ts b/src/lib/seam/access-codes/use-generate-access-code-code.ts index 6820718ef..8ad7f5bce 100644 --- a/src/lib/seam/access-codes/use-generate-access-code-code.ts +++ b/src/lib/seam/access-codes/use-generate-access-code-code.ts @@ -1,5 +1,5 @@ import type { - AccessCodesGenerateCodeBody, + AccessCodesGenerateCodeParams, SeamHttpApiError, } from '@seamapi/http/connect' import { useMutation, type UseMutationResult } from '@tanstack/react-query' @@ -11,7 +11,7 @@ export type UseGenerateAccessCodeCodeParams = never export type UseGenerateAccessCodeCodeData = string export type UseGenerateAccessCodeCodeMutationVariables = - AccessCodesGenerateCodeBody + AccessCodesGenerateCodeParams export function useGenerateAccessCodeCode(): UseMutationResult< UseGenerateAccessCodeCodeData, diff --git a/src/lib/seam/client-sessions/use-client-session.ts b/src/lib/seam/client-sessions/use-client-session.ts index 94c729811..107c7168b 100644 --- a/src/lib/seam/client-sessions/use-client-session.ts +++ b/src/lib/seam/client-sessions/use-client-session.ts @@ -3,13 +3,13 @@ import type { ClientSession } from '@seamapi/types/connect' import { useQuery } from '@tanstack/react-query' import { useSeamClient } from 'lib/seam/use-seam-client.js' -import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js' +import type { UseSeamQueryResultLegacy } from 'lib/seam/use-seam-query-result.js' export type UseClientSessionParams = never export type UseClientSessionData = ClientSession | null -export function useClientSession(): UseSeamQueryResult< +export function useClientSession(): UseSeamQueryResultLegacy< 'clientSession', UseClientSessionData > { diff --git a/src/lib/seam/components/SupportedDeviceTable/use-device-model.ts b/src/lib/seam/components/SupportedDeviceTable/use-device-model.ts index 10cae70af..ef150e7e4 100644 --- a/src/lib/seam/components/SupportedDeviceTable/use-device-model.ts +++ b/src/lib/seam/components/SupportedDeviceTable/use-device-model.ts @@ -7,7 +7,7 @@ import type { import { useQuery } from '@tanstack/react-query' import { useSeamClient } from 'lib/seam/use-seam-client.js' -import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js' +import type { UseSeamQueryResultLegacy } from 'lib/seam/use-seam-query-result.js' export type UseDeviceModelParams = DeviceModelsGetParams @@ -15,7 +15,7 @@ export type UseDeviceModelData = DeviceModel | null export function useDeviceModel( params: UseDeviceModelParams -): UseSeamQueryResult<'deviceModel', UseDeviceModelData> { +): UseSeamQueryResultLegacy<'deviceModel', UseDeviceModelData> { const { client: seam } = useSeamClient() const { data, ...rest } = useQuery({ enabled: seam != null, diff --git a/src/lib/seam/components/SupportedDeviceTable/use-device-models.ts b/src/lib/seam/components/SupportedDeviceTable/use-device-models.ts index 923e6d1b3..e6e3caa98 100644 --- a/src/lib/seam/components/SupportedDeviceTable/use-device-models.ts +++ b/src/lib/seam/components/SupportedDeviceTable/use-device-models.ts @@ -7,7 +7,7 @@ import type { import { useQuery, useQueryClient } from '@tanstack/react-query' import { useSeamClient } from 'lib/seam/use-seam-client.js' -import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js' +import type { UseSeamQueryResultLegacy } from 'lib/seam/use-seam-query-result.js' export type UseDeviceModelsParams = DeviceModelsListParams @@ -15,7 +15,7 @@ export type UseDeviceModelsData = DeviceModel[] export function useDeviceModels( params?: UseDeviceModelsParams -): UseSeamQueryResult<'deviceModels', UseDeviceModelsData> { +): UseSeamQueryResultLegacy<'deviceModels', UseDeviceModelsData> { const { client: seam } = useSeamClient() const queryClient = useQueryClient() diff --git a/src/lib/seam/components/SupportedDeviceTable/use-manufacturer.ts b/src/lib/seam/components/SupportedDeviceTable/use-manufacturer.ts index 28ffdf8a6..b4751cfde 100644 --- a/src/lib/seam/components/SupportedDeviceTable/use-manufacturer.ts +++ b/src/lib/seam/components/SupportedDeviceTable/use-manufacturer.ts @@ -7,7 +7,7 @@ import type { import { useQuery } from '@tanstack/react-query' import { useSeamClient } from 'lib/seam/use-seam-client.js' -import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js' +import type { UseSeamQueryResultLegacy } from 'lib/seam/use-seam-query-result.js' export type UseManufacturerParams = ManufacturersGetParams @@ -15,7 +15,7 @@ export type UseManufacturerData = Manufacturer | null export function useManufacturer( params: UseManufacturerParams -): UseSeamQueryResult<'manufacturer', UseManufacturerData> { +): UseSeamQueryResultLegacy<'manufacturer', UseManufacturerData> { const { client: seam } = useSeamClient() const { data, ...rest } = useQuery({ enabled: seam != null, diff --git a/src/lib/seam/components/SupportedDeviceTable/use-manufacturers.ts b/src/lib/seam/components/SupportedDeviceTable/use-manufacturers.ts index 4cb394fef..18df1027e 100644 --- a/src/lib/seam/components/SupportedDeviceTable/use-manufacturers.ts +++ b/src/lib/seam/components/SupportedDeviceTable/use-manufacturers.ts @@ -7,7 +7,7 @@ import type { import { useQuery, useQueryClient } from '@tanstack/react-query' import { useSeamClient } from 'lib/seam/use-seam-client.js' -import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js' +import type { UseSeamQueryResultLegacy } from 'lib/seam/use-seam-query-result.js' export type UseManufacturersParams = ManufacturersListParams @@ -15,7 +15,7 @@ export type UseManufacturersData = Manufacturer[] export function useManufacturers( params?: UseManufacturersParams -): UseSeamQueryResult<'manufacturers', UseManufacturersData> { +): UseSeamQueryResultLegacy<'manufacturers', UseManufacturersData> { const { client: seam } = useSeamClient() const queryClient = useQueryClient() diff --git a/src/lib/seam/connected-accounts/use-connected-account.ts b/src/lib/seam/connected-accounts/use-connected-account.ts index a43d9db28..b617bb2de 100644 --- a/src/lib/seam/connected-accounts/use-connected-account.ts +++ b/src/lib/seam/connected-accounts/use-connected-account.ts @@ -6,7 +6,7 @@ import type { ConnectedAccount } from '@seamapi/types/connect' import { useQuery } from '@tanstack/react-query' import { useSeamClient } from 'lib/seam/use-seam-client.js' -import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js' +import type { UseSeamQueryResultLegacy } from 'lib/seam/use-seam-query-result.js' export type UseConnectedAccountParams = ConnectedAccountsGetParams @@ -14,7 +14,7 @@ export type UseConnectedAccountData = ConnectedAccount | null export function useConnectedAccount( params: UseConnectedAccountParams -): UseSeamQueryResult<'connectedAccount', UseConnectedAccountData> { +): UseSeamQueryResultLegacy<'connectedAccount', UseConnectedAccountData> { const { client } = useSeamClient() const { data, ...rest } = useQuery( { diff --git a/src/lib/seam/devices/use-device-providers.ts b/src/lib/seam/devices/use-device-providers.ts index 3d1938384..685005f30 100644 --- a/src/lib/seam/devices/use-device-providers.ts +++ b/src/lib/seam/devices/use-device-providers.ts @@ -6,7 +6,7 @@ import type { DeviceProvider } from '@seamapi/types/connect' import { useQuery } from '@tanstack/react-query' import { useSeamClient } from 'lib/seam/use-seam-client.js' -import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js' +import type { UseSeamQueryResultLegacy } from 'lib/seam/use-seam-query-result.js' export type UseDeviceProvidersParams = DevicesListDeviceProvidersParams @@ -14,7 +14,7 @@ export type UseDeviceProvidersData = DeviceProvider[] export function useDeviceProviders( params?: UseDeviceProvidersParams -): UseSeamQueryResult<'deviceProviders', UseDeviceProvidersData> { +): UseSeamQueryResultLegacy<'deviceProviders', UseDeviceProvidersData> { const { client } = useSeamClient() const { data, ...rest } = useQuery({ diff --git a/src/lib/seam/devices/use-device.ts b/src/lib/seam/devices/use-device.ts index 381524f0e..70ac48307 100644 --- a/src/lib/seam/devices/use-device.ts +++ b/src/lib/seam/devices/use-device.ts @@ -3,7 +3,7 @@ import type { Device } from '@seamapi/types/connect' import { useQuery } from '@tanstack/react-query' import { useSeamClient } from 'lib/seam/use-seam-client.js' -import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js' +import type { UseSeamQueryResultLegacy } from 'lib/seam/use-seam-query-result.js' export type UseDeviceParams = DevicesGetParams @@ -11,7 +11,7 @@ export type UseDeviceData = Device | null export function useDevice( params: UseDeviceParams -): UseSeamQueryResult<'device', UseDeviceData> { +): UseSeamQueryResultLegacy<'device', UseDeviceData> { const { client } = useSeamClient() const { data, ...rest } = useQuery({ enabled: client != null, diff --git a/src/lib/seam/devices/use-devices.ts b/src/lib/seam/devices/use-devices.ts index bb7fc02f3..3392678f6 100644 --- a/src/lib/seam/devices/use-devices.ts +++ b/src/lib/seam/devices/use-devices.ts @@ -3,7 +3,7 @@ import type { Device } from '@seamapi/types/connect' import { useQuery, useQueryClient } from '@tanstack/react-query' import { useSeamClient } from 'lib/seam/use-seam-client.js' -import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js' +import type { UseSeamQueryResultLegacy } from 'lib/seam/use-seam-query-result.js' export type UseDevicesParams = DevicesListParams @@ -11,7 +11,7 @@ export type UseDevicesData = Device[] export function useDevices( params?: UseDevicesParams -): UseSeamQueryResult<'devices', UseDevicesData> { +): UseSeamQueryResultLegacy<'devices', UseDevicesData> { const { client } = useSeamClient() const queryClient = useQueryClient() diff --git a/src/lib/seam/events/use-events.ts b/src/lib/seam/events/use-events.ts index 3383c4295..ed89b3b0a 100644 --- a/src/lib/seam/events/use-events.ts +++ b/src/lib/seam/events/use-events.ts @@ -3,7 +3,7 @@ import type { SeamEvent } from '@seamapi/types/connect' import { useQuery, useQueryClient } from '@tanstack/react-query' import { useSeamClient } from 'lib/seam/use-seam-client.js' -import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js' +import type { UseSeamQueryResultLegacy } from 'lib/seam/use-seam-query-result.js' export type UseEventsParams = EventsListParams @@ -16,7 +16,7 @@ export interface UseEventsOptions { export function useEvents( params?: UseEventsParams, { refetchInterval }: UseEventsOptions = {} -): UseSeamQueryResult<'events', UseEventsData> { +): UseSeamQueryResultLegacy<'events', UseEventsData> { const { client } = useSeamClient() const queryClient = useQueryClient() diff --git a/src/lib/seam/index.ts b/src/lib/seam/index.ts index 091e8517d..68df2751c 100644 --- a/src/lib/seam/index.ts +++ b/src/lib/seam/index.ts @@ -12,4 +12,6 @@ export * from './devices/use-device-providers.js' export * from './devices/use-devices.js' export * from './SeamProvider.js' export * from './use-seam-client.js' +export * from './use-seam-mutation.js' +export * from './use-seam-query.js' export * from './use-seam-query-result.js' diff --git a/src/lib/seam/noise-sensors/use-noise-thresholds.ts b/src/lib/seam/noise-sensors/use-noise-thresholds.ts index 535804758..e93fa4247 100644 --- a/src/lib/seam/noise-sensors/use-noise-thresholds.ts +++ b/src/lib/seam/noise-sensors/use-noise-thresholds.ts @@ -6,7 +6,7 @@ import type { NoiseThreshold } from '@seamapi/types/connect' import { useQuery, useQueryClient } from '@tanstack/react-query' import { useSeamClient } from 'lib/seam/use-seam-client.js' -import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js' +import type { UseSeamQueryResultLegacy } from 'lib/seam/use-seam-query-result.js' export type UseNoiseThresholdsParams = NoiseSensorsNoiseThresholdsListParams @@ -14,7 +14,7 @@ export type UseNoiseThresholdsData = NoiseThreshold[] export function useNoiseThresholds( params: UseNoiseThresholdsParams -): UseSeamQueryResult<'noiseThresholds', UseNoiseThresholdsData> { +): UseSeamQueryResultLegacy<'noiseThresholds', UseNoiseThresholdsData> { const { client } = useSeamClient() const queryClient = useQueryClient() diff --git a/src/lib/seam/thermostats/use-delete-thermostat-climate-preset.ts b/src/lib/seam/thermostats/use-delete-thermostat-climate-preset.ts index 9181d7f95..3af8315e1 100644 --- a/src/lib/seam/thermostats/use-delete-thermostat-climate-preset.ts +++ b/src/lib/seam/thermostats/use-delete-thermostat-climate-preset.ts @@ -1,6 +1,6 @@ import type { SeamHttpApiError, - ThermostatsDeleteClimatePresetBody, + ThermostatsDeleteClimatePresetParams, } from '@seamapi/http/connect' import { useMutation, @@ -16,7 +16,7 @@ export type UseDeleteThermostatClimatePresetParams = never export type UseDeleteThermostatClimatePresetData = undefined export type UseDeleteThermostatClimatePresetVariables = - ThermostatsDeleteClimatePresetBody + ThermostatsDeleteClimatePresetParams export function useDeleteThermostatClimatePreset(): UseMutationResult< UseDeleteThermostatClimatePresetData, diff --git a/src/lib/seam/use-seam-client.ts b/src/lib/seam/use-seam-client.ts index 2446e777a..49fe995ee 100644 --- a/src/lib/seam/use-seam-client.ts +++ b/src/lib/seam/use-seam-client.ts @@ -1,12 +1,13 @@ -import { SeamHttp } from '@seamapi/http/connect' +import { SeamHttp, SeamHttpEndpoints } from '@seamapi/http/connect' import { useQuery } from '@tanstack/react-query' import { useEffect } from 'react' import { v4 as uuidv4 } from 'uuid' -import { useSeamContext } from 'lib/seam/SeamProvider.js' +import { useSeamQueryContext } from './SeamQueryProvider.js' export function useSeamClient(): { client: SeamHttp | null + endpointClient: SeamHttpEndpoints | null isPending: boolean isError: boolean error: unknown @@ -17,12 +18,14 @@ export function useSeamClient(): { publishableKey, clientSessionToken, ...context - } = useSeamContext() + } = useSeamQueryContext() const userIdentifierKey = useUserIdentifierKeyOrFingerprint( clientSessionToken != null ? '' : context.userIdentifierKey ) - const { isPending, isError, error, data } = useQuery({ + const { isPending, isError, error, data } = useQuery< + [SeamHttp, SeamHttpEndpoints] + >({ queryKey: [ 'client', { @@ -34,13 +37,19 @@ export function useSeamClient(): { }, ], queryFn: async () => { - if (client != null) return client + if (client != null) + return [client, SeamHttpEndpoints.fromClient(client.client)] if (clientSessionToken != null) { - return SeamHttp.fromClientSessionToken( + const clientSessionTokenClient = SeamHttp.fromClientSessionToken( clientSessionToken, clientOptions ) + + return [ + clientSessionTokenClient, + SeamHttpEndpoints.fromClient(clientSessionTokenClient.client), + ] } if (publishableKey == null) { @@ -49,15 +58,25 @@ export function useSeamClient(): { ) } - return await SeamHttp.fromPublishableKey( + const publishableKeyClient = await SeamHttp.fromPublishableKey( publishableKey, userIdentifierKey, clientOptions ) + return [ + publishableKeyClient, + SeamHttpEndpoints.fromClient(publishableKeyClient.client), + ] }, }) - return { client: data ?? null, isPending, isError, error } + return { + client: data?.[0] ?? null, + endpointClient: data?.[1] ?? null, + isPending, + isError, + error, + } } export class NullSeamClientError extends Error { @@ -65,7 +84,7 @@ export class NullSeamClientError extends Error { super( [ 'Attempted to use a null Seam client.', - 'Either a hook using useSeamClient was called outside of a SeamProvider,', + 'Either a hook using useSeamClient was called outside of a SeamProvider or SeamQueryProvider,', 'or there was an error when creating the Seam client in useSeamClient,', 'or useSeamClient is still loading the client.', ].join(' ') @@ -81,7 +100,7 @@ function useUserIdentifierKeyOrFingerprint( useEffect(() => { if (userIdentifierKey != null) return // eslint-disable-next-line no-console - console.warn(`Using an automatically generated fingerprint for the SeamProvider userIdentifierKey! + console.warn(`Using an automatically generated fingerprint for the Seam userIdentifierKey! The user interface will show warnings when using a fingerprint. This is not recommended because the client session is now bound to this machine and is effectively ephemeral.`) }, [userIdentifierKey]) diff --git a/src/lib/seam/use-seam-mutation.ts b/src/lib/seam/use-seam-mutation.ts new file mode 100644 index 000000000..001290c78 --- /dev/null +++ b/src/lib/seam/use-seam-mutation.ts @@ -0,0 +1,50 @@ +import type { + SeamHttpApiError, + SeamHttpEndpointMutationPaths, + SeamHttpEndpoints, +} from '@seamapi/http/connect' +import { + useMutation, + type UseMutationOptions, + type UseMutationResult, +} from '@tanstack/react-query' + +import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js' + +export type UseSeamMutationVariables = + Parameters[0] + +export type UseSeamMutationResult = + UseMutationResult< + MutationData, + SeamHttpApiError, + UseSeamMutationVariables + > + +export function useSeamMutation( + endpointPath: T, + options: Parameters[1] & + MutationOptions< + MutationData, + SeamHttpApiError, + UseSeamMutationVariables + > = {} +): UseSeamMutationResult { + const { endpointClient: client } = useSeamClient() + return useMutation({ + ...options, + mutationFn: async (variables) => { + if (client === null) throw new NullSeamClientError() + // Using @ts-expect-error over any is preferred, but not possible here because TypeScript will run out of memory. + // Type assertion is needed here for performance reasons. The types are correct at runtime. + const endpoint = client[endpointPath] as (...args: any) => Promise + return await endpoint(variables, options) + }, + }) +} + +type MutationData = Awaited< + ReturnType +> + +type MutationOptions = Omit, 'mutationFn'> diff --git a/src/lib/seam/use-seam-query-result.ts b/src/lib/seam/use-seam-query-result.ts index 834766c8b..79e7bf364 100644 --- a/src/lib/seam/use-seam-query-result.ts +++ b/src/lib/seam/use-seam-query-result.ts @@ -1,7 +1,9 @@ import type { SeamHttpApiError } from '@seamapi/http/connect' import type { UseQueryResult } from '@tanstack/react-query' -export type UseSeamQueryResult = Omit< - UseQueryResult, - 'data' -> & { [key in Field]?: ResponsePayload } +export type UseSeamQueryResultLegacy< + Field extends string, + ResponsePayload, +> = Omit, 'data'> & { + [key in Field]?: ResponsePayload +} diff --git a/src/lib/seam/use-seam-query.ts b/src/lib/seam/use-seam-query.ts new file mode 100644 index 000000000..1fa3a3d8d --- /dev/null +++ b/src/lib/seam/use-seam-query.ts @@ -0,0 +1,45 @@ +import type { + SeamHttpApiError, + SeamHttpEndpointQueryPaths, + SeamHttpEndpoints, +} from '@seamapi/http/connect' +import { + useQuery, + type UseQueryOptions, + type UseQueryResult, +} from '@tanstack/react-query' + +import { useSeamClient } from 'lib/seam/use-seam-client.js' + +export type UseSeamQueryParameters = + Parameters[0] + +export type UseSeamQueryResult = + UseQueryResult, SeamHttpApiError> + +export function useSeamQuery( + endpointPath: T, + parameters?: UseSeamQueryParameters, + options: Parameters[1] & + QueryOptions, SeamHttpApiError> = {} +): UseSeamQueryResult { + const { endpointClient: client } = useSeamClient() + return useQuery({ + enabled: client != null, + ...options, + queryKey: [endpointPath, parameters], + queryFn: async () => { + if (client == null) return null + // Using @ts-expect-error over any is preferred, but not possible here because TypeScript will run out of memory. + // Type assertion is needed here for performance reasons. The types are correct at runtime. + const endpoint = client[endpointPath] as (...args: any) => Promise + return await endpoint(parameters, options) + }, + }) +} + +type QueryData = Awaited< + ReturnType +> + +type QueryOptions = Omit, 'queryKey' | 'queryFn'> diff --git a/src/lib/telemetry/hooks.ts b/src/lib/telemetry/hooks.ts index 469595807..7a0c403e7 100644 --- a/src/lib/telemetry/hooks.ts +++ b/src/lib/telemetry/hooks.ts @@ -1,7 +1,7 @@ import { useEffect, useLayoutEffect } from 'react' import { useClientSession } from 'lib/seam/client-sessions/use-client-session.js' -import { useSeamContext } from 'lib/seam/SeamProvider.js' +import { useSeamQueryContext } from 'lib/seam/SeamQueryProvider.js' import type { TelemetryClient } from './client.js' import { useTelemetryContext } from './TelemetryProvider.js' @@ -23,7 +23,7 @@ export function useComponentTelemetry(name: string): void { export function useUserTelemetry(): void { const telemetry = useTelemetryClient() - const { publishableKey } = useSeamContext() + const { publishableKey } = useSeamQueryContext() const { clientSession } = useClientSession() // Ensure identify runs earlier than other effects to avoid anonymous telemetry data.