Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@ $ 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 react react-dom
```

If using TypeScript, install these packages as development dependencies:

```
@seamapi/types @types/react @types/react-dom
```

## Usage

### With React
Expand Down
6,724 changes: 3,333 additions & 3,391 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -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'
211 changes: 28 additions & 183 deletions src/lib/seam/SeamProvider.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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
Expand All @@ -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
}

Expand All @@ -70,12 +60,6 @@ interface SeamProviderBaseProps extends PropsWithChildren {
onSessionUpdate?: (client: SeamHttp) => void
}

type SeamClientOptions = SeamHttpOptionsWithClientSessionToken

export type SeamProviderClientOptions = Pick<SeamClientOptions, 'endpoint'>

const defaultQueryClient = new QueryClient()

export const seamComponentsClassName = 'seam-components'

export function SeamProvider({
Expand All @@ -84,198 +68,59 @@ 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 (
<div className={seamComponentsClassName}>
<TelemetryProvider
client={telemetryClient ?? globalThis.seamTelemetryClient}
disabled={disableTelemetry}
endpoint={endpoint}
>
<QueryClientProvider
client={
queryClient ?? globalThis.seamQueryClient ?? defaultQueryClient
}
>
<SeamQueryProvider {...props}>
<Provider value={value}>
<Wrapper onSessionUpdate={onSessionUpdate}>{children}</Wrapper>
<Telemetry>{children}</Telemetry>
</Provider>
</QueryClientProvider>
</SeamQueryProvider>
</TelemetryProvider>
</div>
)
}

function Wrapper({
onSessionUpdate,
children,
}: Required<Pick<SeamProviderProps, 'onSessionUpdate'>> &
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<SeamContext>(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<SeamContext>(defaultSeamContextValue)
Loading
Loading