|
| 1 | +import type { |
| 2 | + SeamHttp, |
| 3 | + SeamHttpEndpoints, |
| 4 | + SeamHttpOptionsWithClientSessionToken, |
| 5 | +} from '@seamapi/http/connect' |
| 6 | +import { |
| 7 | + QueryClient, |
| 8 | + QueryClientContext, |
| 9 | + QueryClientProvider, |
| 10 | +} from '@tanstack/react-query' |
| 11 | +import { |
| 12 | + createContext, |
| 13 | + type JSX, |
| 14 | + type PropsWithChildren, |
| 15 | + useContext, |
| 16 | + useEffect, |
| 17 | + useMemo, |
| 18 | +} from 'react' |
| 19 | + |
| 20 | +import { useSeamClient } from './use-seam-client.js' |
| 21 | + |
| 22 | +export interface SeamQueryContext { |
| 23 | + client: SeamHttp | null |
| 24 | + endpointClient: SeamHttpEndpoints | null |
| 25 | + clientOptions?: SeamQueryProviderClientOptions | undefined |
| 26 | + publishableKey?: string | undefined |
| 27 | + userIdentifierKey?: string | undefined |
| 28 | + clientSessionToken?: string | undefined |
| 29 | + consoleSessionToken?: string | undefined |
| 30 | + workspaceId?: string | undefined |
| 31 | + queryKeyPrefix?: string | undefined |
| 32 | +} |
| 33 | + |
| 34 | +export type SeamQueryProviderProps = |
| 35 | + | SeamQueryProviderPropsWithClient |
| 36 | + | SeamQueryProviderPropsWithPublishableKey |
| 37 | + | SeamQueryProviderPropsWithClientSessionToken |
| 38 | + | SeamQueryProviderPropsWithConsoleSessionToken |
| 39 | + |
| 40 | +export interface SeamQueryProviderPropsWithClient |
| 41 | + extends SeamQueryProviderBaseProps { |
| 42 | + client: SeamHttp |
| 43 | + queryKeyPrefix: string |
| 44 | +} |
| 45 | + |
| 46 | +export interface SeamQueryProviderPropsWithPublishableKey |
| 47 | + extends SeamQueryProviderBaseProps, |
| 48 | + SeamQueryProviderClientOptions { |
| 49 | + publishableKey: string |
| 50 | + userIdentifierKey?: string |
| 51 | +} |
| 52 | + |
| 53 | +export interface SeamQueryProviderPropsWithClientSessionToken |
| 54 | + extends SeamQueryProviderBaseProps, |
| 55 | + SeamQueryProviderClientOptions { |
| 56 | + clientSessionToken: string |
| 57 | +} |
| 58 | + |
| 59 | +export interface SeamQueryProviderPropsWithConsoleSessionToken |
| 60 | + extends SeamQueryProviderBaseProps, |
| 61 | + SeamQueryProviderClientOptions { |
| 62 | + consoleSessionToken: string |
| 63 | + workspaceId?: string | undefined |
| 64 | +} |
| 65 | + |
| 66 | +interface SeamQueryProviderBaseProps extends PropsWithChildren { |
| 67 | + queryClient?: QueryClient | undefined |
| 68 | + onSessionUpdate?: (client: SeamHttp) => void |
| 69 | +} |
| 70 | + |
| 71 | +type SeamClientOptions = SeamHttpOptionsWithClientSessionToken |
| 72 | + |
| 73 | +export type SeamQueryProviderClientOptions = Pick< |
| 74 | + SeamClientOptions, |
| 75 | + 'endpoint' | 'isUndocumentedApiEnabled' |
| 76 | +> |
| 77 | + |
| 78 | +const defaultQueryClient = new QueryClient() |
| 79 | + |
| 80 | +export function SeamQueryProvider({ |
| 81 | + children, |
| 82 | + onSessionUpdate = () => {}, |
| 83 | + queryClient, |
| 84 | + ...props |
| 85 | +}: SeamQueryProviderProps): JSX.Element { |
| 86 | + const value = useMemo(() => { |
| 87 | + const context = createSeamQueryContextValue(props) |
| 88 | + if ( |
| 89 | + context.client == null && |
| 90 | + context.publishableKey == null && |
| 91 | + context.clientSessionToken == null && |
| 92 | + context.consoleSessionToken == null |
| 93 | + ) { |
| 94 | + return defaultSeamQueryContextValue |
| 95 | + } |
| 96 | + return context |
| 97 | + }, [props]) |
| 98 | + |
| 99 | + if ( |
| 100 | + value.client == null && |
| 101 | + value.publishableKey == null && |
| 102 | + value.clientSessionToken == null && |
| 103 | + value.consoleSessionToken == null |
| 104 | + ) { |
| 105 | + throw new Error( |
| 106 | + `Must provide either a Seam client, clientSessionToken, publishableKey or consoleSessionToken.`, |
| 107 | + ) |
| 108 | + } |
| 109 | + |
| 110 | + const { Provider } = seamContext |
| 111 | + const queryClientFromContext = useContext(QueryClientContext) |
| 112 | + |
| 113 | + if ( |
| 114 | + queryClientFromContext != null && |
| 115 | + queryClient != null && |
| 116 | + queryClientFromContext !== queryClient |
| 117 | + ) { |
| 118 | + throw new Error( |
| 119 | + 'The QueryClient passed into SeamQueryProvider is different from the one in the existing QueryClientContext. Omit the queryClient prop from SeamProvider or SeamQueryProvider to use the existing QueryClient provided by the QueryClientProvider.', |
| 120 | + ) |
| 121 | + } |
| 122 | + |
| 123 | + return ( |
| 124 | + <QueryClientProvider |
| 125 | + client={queryClientFromContext ?? queryClient ?? defaultQueryClient} |
| 126 | + > |
| 127 | + <Provider value={value}> |
| 128 | + <Session onSessionUpdate={onSessionUpdate}>{children}</Session> |
| 129 | + </Provider> |
| 130 | + </QueryClientProvider> |
| 131 | + ) |
| 132 | +} |
| 133 | + |
| 134 | +function Session({ |
| 135 | + onSessionUpdate, |
| 136 | + children, |
| 137 | +}: Required<Pick<SeamQueryProviderProps, 'onSessionUpdate'>> & |
| 138 | + PropsWithChildren): JSX.Element | null { |
| 139 | + const { client } = useSeamClient() |
| 140 | + useEffect(() => { |
| 141 | + if (client != null) onSessionUpdate(client) |
| 142 | + }, [onSessionUpdate, client]) |
| 143 | + |
| 144 | + return <>{children}</> |
| 145 | +} |
| 146 | + |
| 147 | +const createDefaultSeamQueryContextValue = (): SeamQueryContext => { |
| 148 | + return { client: null, endpointClient: null } |
| 149 | +} |
| 150 | + |
| 151 | +const createSeamQueryContextValue = ( |
| 152 | + options: SeamQueryProviderProps, |
| 153 | +): SeamQueryContext => { |
| 154 | + if (isSeamQueryProviderPropsWithClient(options)) { |
| 155 | + if (options.queryKeyPrefix == null) { |
| 156 | + throw new InvalidSeamQueryProviderProps( |
| 157 | + 'The client prop must be used with a queryKeyPrefix prop.', |
| 158 | + ) |
| 159 | + } |
| 160 | + return { |
| 161 | + ...options, |
| 162 | + endpointClient: null, |
| 163 | + } |
| 164 | + } |
| 165 | + |
| 166 | + if (isSeamQueryProviderPropsWithClientSessionToken(options)) { |
| 167 | + const { clientSessionToken, ...clientOptions } = options |
| 168 | + return { |
| 169 | + clientSessionToken, |
| 170 | + clientOptions, |
| 171 | + client: null, |
| 172 | + endpointClient: null, |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + if (isSeamQueryProviderPropsWithPublishableKey(options)) { |
| 177 | + const { publishableKey, userIdentifierKey, ...clientOptions } = options |
| 178 | + return { |
| 179 | + publishableKey, |
| 180 | + userIdentifierKey, |
| 181 | + clientOptions, |
| 182 | + client: null, |
| 183 | + endpointClient: null, |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + if (isSeamQueryProviderPropsWithConsoleSessionToken(options)) { |
| 188 | + const { consoleSessionToken, workspaceId, ...clientOptions } = options |
| 189 | + return { |
| 190 | + consoleSessionToken, |
| 191 | + workspaceId, |
| 192 | + clientOptions, |
| 193 | + client: null, |
| 194 | + endpointClient: null, |
| 195 | + } |
| 196 | + } |
| 197 | + |
| 198 | + return { client: null, endpointClient: null } |
| 199 | +} |
| 200 | + |
| 201 | +const defaultSeamQueryContextValue = createDefaultSeamQueryContextValue() |
| 202 | + |
| 203 | +export const seamContext = createContext<SeamQueryContext>( |
| 204 | + defaultSeamQueryContextValue, |
| 205 | +) |
| 206 | + |
| 207 | +export function useSeamQueryContext(): SeamQueryContext { |
| 208 | + return useContext(seamContext) |
| 209 | +} |
| 210 | + |
| 211 | +const isSeamQueryProviderPropsWithClient = ( |
| 212 | + props: SeamQueryProviderProps, |
| 213 | +): props is SeamQueryProviderPropsWithClient => { |
| 214 | + if (!('client' in props)) return false |
| 215 | + |
| 216 | + const { client, ...otherProps } = props |
| 217 | + if (client == null) return false |
| 218 | + |
| 219 | + const otherNonNullProps = Object.values(otherProps).filter((v) => v != null) |
| 220 | + if (otherNonNullProps.length > 0) { |
| 221 | + throw new InvalidSeamQueryProviderProps( |
| 222 | + `The client prop cannot be used with ${otherNonNullProps.join(' or ')}.`, |
| 223 | + ) |
| 224 | + } |
| 225 | + |
| 226 | + return true |
| 227 | +} |
| 228 | + |
| 229 | +const isSeamQueryProviderPropsWithPublishableKey = ( |
| 230 | + props: SeamQueryProviderProps, |
| 231 | +): props is SeamQueryProviderPropsWithPublishableKey & |
| 232 | + SeamQueryProviderClientOptions => { |
| 233 | + if (!('publishableKey' in props)) return false |
| 234 | + |
| 235 | + const { publishableKey } = props |
| 236 | + if (publishableKey == null) return false |
| 237 | + |
| 238 | + if ('client' in props && props.client != null) { |
| 239 | + throw new InvalidSeamQueryProviderProps( |
| 240 | + 'The client prop cannot be used with the publishableKey prop.', |
| 241 | + ) |
| 242 | + } |
| 243 | + |
| 244 | + if ('clientSessionToken' in props && props.clientSessionToken != null) { |
| 245 | + throw new InvalidSeamQueryProviderProps( |
| 246 | + 'The clientSessionToken prop cannot be used with the publishableKey prop.', |
| 247 | + ) |
| 248 | + } |
| 249 | + |
| 250 | + if ('consoleSessionToken' in props && props.consoleSessionToken != null) { |
| 251 | + throw new InvalidSeamQueryProviderProps( |
| 252 | + 'The consoleSessionToken prop cannot be used with the publishableKey prop.', |
| 253 | + ) |
| 254 | + } |
| 255 | + |
| 256 | + if ('workspaceId' in props && props.workspaceId != null) { |
| 257 | + throw new InvalidSeamQueryProviderProps( |
| 258 | + 'The workspaceId prop cannot be used with the publishableKey prop.', |
| 259 | + ) |
| 260 | + } |
| 261 | + |
| 262 | + return true |
| 263 | +} |
| 264 | + |
| 265 | +const isSeamQueryProviderPropsWithClientSessionToken = ( |
| 266 | + props: SeamQueryProviderProps, |
| 267 | +): props is SeamQueryProviderPropsWithClientSessionToken & |
| 268 | + SeamQueryProviderClientOptions => { |
| 269 | + if (!('clientSessionToken' in props)) return false |
| 270 | + |
| 271 | + const { clientSessionToken } = props |
| 272 | + if (clientSessionToken == null) return false |
| 273 | + |
| 274 | + if ('client' in props && props.client != null) { |
| 275 | + throw new InvalidSeamQueryProviderProps( |
| 276 | + 'The client prop cannot be used with the clientSessionToken prop.', |
| 277 | + ) |
| 278 | + } |
| 279 | + |
| 280 | + if ('publishableKey' in props && props.publishableKey != null) { |
| 281 | + throw new InvalidSeamQueryProviderProps( |
| 282 | + 'The publishableKey prop cannot be used with the clientSessionToken prop.', |
| 283 | + ) |
| 284 | + } |
| 285 | + |
| 286 | + if ('userIdentifierKey' in props && props.userIdentifierKey != null) { |
| 287 | + throw new InvalidSeamQueryProviderProps( |
| 288 | + 'The userIdentifierKey prop cannot be used with the clientSessionToken prop.', |
| 289 | + ) |
| 290 | + } |
| 291 | + |
| 292 | + if ('consoleSessionToken' in props && props.consoleSessionToken != null) { |
| 293 | + throw new InvalidSeamQueryProviderProps( |
| 294 | + 'The consoleSessionToken prop cannot be used with the clientSessionToken prop.', |
| 295 | + ) |
| 296 | + } |
| 297 | + |
| 298 | + if ('workspaceId' in props && props.workspaceId != null) { |
| 299 | + throw new InvalidSeamQueryProviderProps( |
| 300 | + 'The workspaceId prop cannot be used with the clientSessionToken prop.', |
| 301 | + ) |
| 302 | + } |
| 303 | + |
| 304 | + return true |
| 305 | +} |
| 306 | + |
| 307 | +const isSeamQueryProviderPropsWithConsoleSessionToken = ( |
| 308 | + props: SeamQueryProviderProps, |
| 309 | +): props is SeamQueryProviderPropsWithConsoleSessionToken & |
| 310 | + SeamQueryProviderClientOptions => { |
| 311 | + if (!('consoleSessionToken' in props)) return false |
| 312 | + |
| 313 | + const { consoleSessionToken } = props |
| 314 | + if (consoleSessionToken == null) return false |
| 315 | + |
| 316 | + if ('client' in props && props.client != null) { |
| 317 | + throw new InvalidSeamQueryProviderProps( |
| 318 | + 'The client prop cannot be used with the publishableKey prop.', |
| 319 | + ) |
| 320 | + } |
| 321 | + |
| 322 | + if ('clientSessionToken' in props && props.clientSessionToken != null) { |
| 323 | + throw new InvalidSeamQueryProviderProps( |
| 324 | + 'The clientSessionToken prop cannot be used with the publishableKey prop.', |
| 325 | + ) |
| 326 | + } |
| 327 | + |
| 328 | + if ('publishableKey' in props && props.publishableKey != null) { |
| 329 | + throw new InvalidSeamQueryProviderProps( |
| 330 | + 'The publishableKey prop cannot be used with the consoleSessionToken prop.', |
| 331 | + ) |
| 332 | + } |
| 333 | + |
| 334 | + if ('userIdentifierKey' in props && props.userIdentifierKey != null) { |
| 335 | + throw new InvalidSeamQueryProviderProps( |
| 336 | + 'The userIdentifierKey prop cannot be used with the consoleSessionToken prop.', |
| 337 | + ) |
| 338 | + } |
| 339 | + |
| 340 | + return true |
| 341 | +} |
| 342 | + |
| 343 | +class InvalidSeamQueryProviderProps extends Error { |
| 344 | + constructor(message: string) { |
| 345 | + super(`SeamQueryProvider received invalid props: ${message}`) |
| 346 | + this.name = this.constructor.name |
| 347 | + } |
| 348 | +} |
0 commit comments