@quiltt/react 2.0.0 → 2.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +474 -0
  2. package/README.md +110 -75
  3. package/dist/QuilttContainer-3c14e032.d.ts +24 -0
  4. package/dist/{QuilttSettingsProvider-6904aa95.d.ts → QuilttSettingsProvider-d1d44353.d.ts} +0 -1
  5. package/dist/components/index.cjs +1 -1
  6. package/dist/components/index.cjs.map +1 -1
  7. package/dist/components/index.d.ts +2 -1
  8. package/dist/components/index.js +1 -1
  9. package/dist/components/index.js.map +1 -1
  10. package/dist/index.cjs +1 -1
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts +3 -2
  13. package/dist/index.js +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/providers/index.cjs +1 -1
  16. package/dist/providers/index.cjs.map +1 -1
  17. package/dist/providers/index.d.ts +1 -1
  18. package/dist/providers/index.js +1 -1
  19. package/dist/providers/index.js.map +1 -1
  20. package/package.json +13 -3
  21. package/src/components/QuilttButton.tsx +24 -0
  22. package/src/components/QuilttContainer.tsx +30 -0
  23. package/src/components/index.ts +2 -0
  24. package/src/hooks/helpers/index.ts +2 -0
  25. package/src/hooks/helpers/useEventListener.ts +84 -0
  26. package/src/hooks/helpers/useIsomorphicLayoutEffect.ts +11 -0
  27. package/src/hooks/index.ts +8 -0
  28. package/src/hooks/session/index.ts +4 -0
  29. package/src/hooks/session/useAuthenticateSession.ts +54 -0
  30. package/src/hooks/session/useIdentifySession.ts +55 -0
  31. package/src/hooks/session/useImportSession.ts +43 -0
  32. package/src/hooks/session/useRevokeSession.ts +27 -0
  33. package/src/hooks/useQuilttClient.ts +5 -0
  34. package/src/hooks/useQuilttConnector.ts +39 -0
  35. package/src/hooks/useQuilttSession.ts +45 -0
  36. package/src/hooks/useQuilttSettings.ts +17 -0
  37. package/src/hooks/useSession.ts +69 -0
  38. package/src/hooks/useStorage.ts +73 -0
  39. package/src/index.ts +4 -0
  40. package/src/providers/QuilttAuthProvider.tsx +43 -0
  41. package/src/providers/QuilttProvider.tsx +19 -0
  42. package/src/providers/QuilttSettingsProvider.tsx +20 -0
  43. package/src/providers/index.ts +3 -0
  44. package/src/types.ts +9 -0
  45. package/dist/QuilttContainer-f0724678.d.ts +0 -15
@@ -0,0 +1,55 @@
1
+ import { useCallback } from 'react'
2
+
3
+ import type {
4
+ AuthAPI,
5
+ SessionResponse,
6
+ UnprocessableData,
7
+ UnprocessableResponse,
8
+ UsernamePayload,
9
+ } from '@quiltt/core'
10
+
11
+ import type { SetSession } from '../useSession'
12
+
13
+ type IdentifySessionCallbacks = {
14
+ onSuccess?: () => unknown
15
+ onChallenged?: () => unknown
16
+ onError?: (errors: UnprocessableData) => unknown
17
+ }
18
+ type IdentifySession = (
19
+ payload: UsernamePayload,
20
+ callbacks: IdentifySessionCallbacks
21
+ ) => Promise<unknown>
22
+
23
+ type UseIdentifySession = (auth: AuthAPI, setSession: SetSession) => IdentifySession
24
+
25
+ export const useIdentifySession: UseIdentifySession = (auth, setSession) => {
26
+ const identifySession = useCallback<IdentifySession>(
27
+ async (payload, callbacks) => {
28
+ const response = await auth.identify(payload)
29
+ const unprocessableResponse = response as UnprocessableResponse
30
+
31
+ switch (response.status) {
32
+ case 201:
33
+ setSession((response as SessionResponse).data.token)
34
+ if (callbacks.onSuccess) return callbacks.onSuccess()
35
+ break
36
+
37
+ case 202:
38
+ if (callbacks.onChallenged) return callbacks.onChallenged()
39
+ break
40
+
41
+ case 422:
42
+ if (callbacks.onError) return callbacks.onError(unprocessableResponse.data)
43
+ break
44
+
45
+ default:
46
+ throw new Error(`Unexpected auth identify response status: ${response.status}`)
47
+ }
48
+ },
49
+ [auth, setSession]
50
+ )
51
+
52
+ return identifySession
53
+ }
54
+
55
+ export default useIdentifySession
@@ -0,0 +1,43 @@
1
+ import { useCallback } from 'react'
2
+
3
+ import type { AuthAPI, Maybe, QuilttJWT } from '@quiltt/core'
4
+
5
+ import type { SetSession } from '../useSession'
6
+
7
+ type ImportSession = (token: string) => Promise<boolean>
8
+
9
+ type UseImportSession = (
10
+ auth: AuthAPI,
11
+ session: Maybe<QuilttJWT> | undefined,
12
+ setSession: SetSession
13
+ ) => ImportSession
14
+
15
+ export const useImportSession: UseImportSession = (auth, session, setSession) => {
16
+ const importSession = useCallback<ImportSession>(
17
+ async (token) => {
18
+ if (!token) return !!session
19
+ if (session && session.token == token) return true
20
+
21
+ const response = await auth.ping(token)
22
+
23
+ switch (response.status) {
24
+ case 200:
25
+ setSession(token)
26
+ return true
27
+ break
28
+
29
+ case 401:
30
+ return false
31
+ break
32
+
33
+ default:
34
+ throw new Error(`Unexpected auth ping response status: ${response.status}`)
35
+ }
36
+ },
37
+ [auth, session, setSession]
38
+ )
39
+
40
+ return importSession
41
+ }
42
+
43
+ export default useImportSession
@@ -0,0 +1,27 @@
1
+ import { useCallback } from 'react'
2
+
3
+ import type { AuthAPI, Maybe, QuilttJWT } from '@quiltt/core'
4
+
5
+ import type { SetSession } from '../useSession'
6
+
7
+ type RevokeSession = () => Promise<void>
8
+
9
+ type UseRevokeSession = (
10
+ auth: AuthAPI,
11
+ session: Maybe<QuilttJWT> | undefined,
12
+ setSession: SetSession
13
+ ) => RevokeSession
14
+
15
+ export const useRevokeSession: UseRevokeSession = (auth, session, setSession) => {
16
+ const revokeSession = useCallback<RevokeSession>(async () => {
17
+ if (!session) return
18
+
19
+ await auth.revoke(session.token)
20
+
21
+ setSession(null)
22
+ }, [auth, session, setSession])
23
+
24
+ return revokeSession
25
+ }
26
+
27
+ export default useRevokeSession
@@ -0,0 +1,5 @@
1
+ import { useApolloClient } from '@apollo/client/index.js'
2
+
3
+ export const useQuilttClient = useApolloClient
4
+
5
+ export default useQuilttClient
@@ -0,0 +1,39 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+ import { useQuilttSession } from './useQuilttSession'
5
+
6
+ const QUILTT_CDN_BASE = process.env.QUILTT_CDN_BASE || 'https://cdn.quiltt.io'
7
+
8
+ // Script Element Singleton
9
+ let scriptElement: HTMLScriptElement
10
+
11
+ export const useQuilttConnector = () => {
12
+ const { session } = useQuilttSession()
13
+
14
+ // Create Script Element
15
+ useEffect(() => {
16
+ if (scriptElement) return
17
+
18
+ scriptElement = document.createElement('script')
19
+ scriptElement.src = `${QUILTT_CDN_BASE}/v1/connector.js`
20
+
21
+ if (session?.token) {
22
+ scriptElement.setAttribute('quiltt-token', session.token)
23
+ }
24
+
25
+ document.head.appendChild(scriptElement)
26
+ // eslint-disable-next-line react-hooks/exhaustive-deps
27
+ }, [])
28
+
29
+ // Update Script Element
30
+ useEffect(() => {
31
+ if (!scriptElement) return
32
+
33
+ if (session?.token && session.token !== scriptElement.getAttribute('quiltt-token')) {
34
+ scriptElement.setAttribute('quiltt-token', session.token)
35
+ } else if (!session?.token && scriptElement.getAttribute('quiltt-token')) {
36
+ scriptElement.removeAttribute('quiltt-token')
37
+ }
38
+ }, [session?.token])
39
+ }
@@ -0,0 +1,45 @@
1
+ import { useCallback } from 'react'
2
+
3
+ import { useSession } from './useSession'
4
+
5
+ import { AuthAPI } from '@quiltt/core'
6
+
7
+ import {
8
+ useAuthenticateSession,
9
+ useIdentifySession,
10
+ useImportSession,
11
+ useRevokeSession,
12
+ } from './session'
13
+ import { useQuilttSettings } from './useQuilttSettings'
14
+
15
+ export const useQuilttSession = () => {
16
+ const { clientId } = useQuilttSettings()
17
+ const [session, setSession] = useSession()
18
+
19
+ const auth = new AuthAPI(clientId)
20
+ const importSession = useImportSession(auth, session, setSession)
21
+ const identifySession = useIdentifySession(auth, setSession)
22
+ const authenticateSession = useAuthenticateSession(auth, setSession)
23
+ const revokeSession = useRevokeSession(auth, session, setSession)
24
+
25
+ // Optionally takes a token, to help guard against async processes clearing the wrong session
26
+ const forgetSession = useCallback(
27
+ async (token?: string) => {
28
+ if (!token || (session && token && token == session.token)) {
29
+ setSession(null)
30
+ }
31
+ },
32
+ [session, setSession]
33
+ )
34
+
35
+ return {
36
+ session,
37
+ importSession,
38
+ identifySession,
39
+ authenticateSession,
40
+ revokeSession,
41
+ forgetSession,
42
+ }
43
+ }
44
+
45
+ export default useQuilttSession
@@ -0,0 +1,17 @@
1
+ 'use client'
2
+
3
+ import { createContext, useContext } from 'react'
4
+
5
+ type QuilttSettingsContext = {
6
+ clientId?: string | undefined
7
+ }
8
+
9
+ export const QuilttSettings = createContext<QuilttSettingsContext>({})
10
+
11
+ export const useQuilttSettings = () => {
12
+ const settings = useContext(QuilttSettings)
13
+
14
+ return { ...settings }
15
+ }
16
+
17
+ export default useQuilttSettings
@@ -0,0 +1,69 @@
1
+ 'use client'
2
+
3
+ import { Dispatch, SetStateAction, useMemo } from 'react'
4
+ import { useEffect, useCallback } from 'react'
5
+
6
+ import type { Maybe, PrivateClaims, QuilttJWT } from '@quiltt/core'
7
+ import { JsonWebTokenParse, Timeoutable } from '@quiltt/core'
8
+
9
+ import { useStorage } from './useStorage'
10
+
11
+ export type SetSession = Dispatch<SetStateAction<Maybe<string> | undefined>>
12
+
13
+ const parse = JsonWebTokenParse<PrivateClaims>
14
+
15
+ /**
16
+ * Singleton timeout, allows hooks to come and go, while ensuring that there is
17
+ * one notification being sent, preventing race conditions.
18
+ */
19
+ const sessionTimer = new Timeoutable()
20
+
21
+ /**
22
+ * useSession uses useStorage to support a global singleton style of access. When
23
+ * updated, all components, and windows should also invalidate.
24
+ *
25
+ * TODO: Support Rotation before Expiry
26
+ *
27
+ * Dataflow can come from two directions:
28
+ * 1. Login - Bottom Up
29
+ * This happens on login, when a token is passed up through the setSession
30
+ * callback. From here it needs to be stored, and shared for usage.
31
+ * 2. Refresh - Top Down
32
+ * This happens when a page is reloaded or a person returns, and everything is
33
+ * reinitialized.
34
+ */
35
+ export const useSession = (): [Maybe<QuilttJWT> | undefined, SetSession] => {
36
+ const [token, setToken] = useStorage<string>('session')
37
+ const session = useMemo(() => parse(token), [token])
38
+
39
+ // Clear session if/when it expires
40
+ useEffect(() => {
41
+ if (!session) return
42
+
43
+ const expirationMS = session.claims.exp * 1000
44
+ const expire = () => setToken(null)
45
+
46
+ if (Date.now() >= expirationMS) {
47
+ expire()
48
+ } else {
49
+ sessionTimer.set(expire, expirationMS - Date.now())
50
+ return () => sessionTimer.clear(expire)
51
+ }
52
+ }, [session, setToken])
53
+
54
+ // Bubbles up from Login
55
+ const setSession = useCallback(
56
+ (nextState: Maybe<string> | SetStateAction<Maybe<string> | undefined> | undefined) => {
57
+ const newState = nextState instanceof Function ? nextState(token) : nextState
58
+
59
+ if (token !== newState && (!newState || parse(newState))) {
60
+ setToken(newState)
61
+ }
62
+ },
63
+ [token, setToken]
64
+ )
65
+
66
+ return [session, setSession]
67
+ }
68
+
69
+ export default useSession
@@ -0,0 +1,73 @@
1
+ 'use client'
2
+
3
+ import type { Dispatch, SetStateAction } from 'react'
4
+ import { useCallback, useEffect, useState } from 'react'
5
+
6
+ import type { Maybe } from '@quiltt/core'
7
+ import { GlobalStorage } from '@quiltt/core'
8
+
9
+ /**
10
+ * Attempt to persist state with local storage, so it remains after refresh and
11
+ * across open documents. Falls back to in memory storage when localStorage is
12
+ * unavailable.
13
+ *
14
+ * This hook is used in the same way as useState except that you must pass the
15
+ * storage key in the 1st parameter. If the window object is not present (as in SSR),
16
+ * useStorage() will return the default nextState.
17
+ *
18
+ * Expect values to remain in sync
19
+ * Across Hooks
20
+ * Across Reloads
21
+ * Across Windows (Documents)
22
+ *
23
+ * @param key
24
+ * @param initialState
25
+ * @returns {Array} [storage, setStorage]
26
+ */
27
+ export const useStorage = <T>(
28
+ key: string,
29
+ initialState?: Maybe<T>
30
+ ): [Maybe<T> | undefined, Dispatch<SetStateAction<Maybe<T> | undefined>>] => {
31
+ const getStorage = useCallback(() => {
32
+ let state
33
+
34
+ if ((state = GlobalStorage.get(key)) !== undefined) {
35
+ return state
36
+ }
37
+
38
+ return initialState
39
+ }, [key, initialState])
40
+
41
+ const [hookState, setHookState] = useState<Maybe<T> | undefined>(getStorage())
42
+
43
+ const setStorage = useCallback(
44
+ (nextState: Maybe<T> | SetStateAction<Maybe<T> | undefined>) => {
45
+ const newState = nextState instanceof Function ? nextState(hookState) : nextState
46
+
47
+ if (hookState !== newState) {
48
+ GlobalStorage.set(key, newState)
49
+ }
50
+ },
51
+ [key, hookState]
52
+ )
53
+
54
+ /**
55
+ * The empty dependency array ensures that the effect runs only once when the component mounts
56
+ * and doesn't re-run unnecessarily on subsequent renders because it doesn't depend on any
57
+ * props or state variables that could change during the component's lifetime.
58
+ *
59
+ * Use an empty dependency array to avoid unnecessary re-renders.
60
+ */
61
+ useEffect(() => {
62
+ GlobalStorage.subscribe(key, setHookState)
63
+
64
+ setHookState(getStorage())
65
+
66
+ return () => GlobalStorage.unsubscribe(key, setHookState)
67
+ // eslint-disable-next-line react-hooks/exhaustive-deps
68
+ }, [])
69
+
70
+ return [hookState, setStorage]
71
+ }
72
+
73
+ export default useStorage
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from '@quiltt/core'
2
+ export * from './hooks'
3
+ export * from './providers'
4
+ export * from './components'
@@ -0,0 +1,43 @@
1
+ 'use client'
2
+
3
+ import type { FC, PropsWithChildren } from 'react'
4
+ import { useEffect } from 'react'
5
+
6
+ import { ApolloProvider } from '@apollo/client'
7
+ import { InMemoryCache, QuilttClient } from '@quiltt/core'
8
+ import { useQuilttSession } from '../hooks'
9
+
10
+ type QuilttAuthProviderProps = PropsWithChildren & {
11
+ token?: string
12
+ }
13
+
14
+ const Client = new QuilttClient({
15
+ cache: new InMemoryCache(),
16
+ })
17
+
18
+ /**
19
+ * If a token is provided, will validate the token against the api and then import
20
+ * it into trusted storage. While this process is happening, the component is put
21
+ * into a loading state and the children are not rendered to prevent race conditions
22
+ * from triggering within the transitionary state.
23
+ *
24
+ */
25
+ export const QuilttAuthProvider: FC<QuilttAuthProviderProps> = ({ token, children }) => {
26
+ const { session, importSession } = useQuilttSession()
27
+
28
+ // Import Passed in Tokens
29
+ useEffect(() => {
30
+ if (token) importSession(token)
31
+ // eslint-disable-next-line react-hooks/exhaustive-deps
32
+ }, [token])
33
+
34
+ // Reset Client Store when logging in or out
35
+ useEffect(() => {
36
+ Client.resetStore()
37
+ // eslint-disable-next-line react-hooks/exhaustive-deps
38
+ }, [session])
39
+
40
+ return <ApolloProvider client={Client}>{children}</ApolloProvider>
41
+ }
42
+
43
+ export default QuilttAuthProvider
@@ -0,0 +1,19 @@
1
+ import type { FC, PropsWithChildren } from 'react'
2
+
3
+ import { QuilttAuthProvider } from './QuilttAuthProvider'
4
+ import { QuilttSettingsProvider } from './QuilttSettingsProvider'
5
+
6
+ type QuilttProviderProps = PropsWithChildren & {
7
+ clientId?: string
8
+ token?: string
9
+ }
10
+
11
+ export const QuilttProvider: FC<QuilttProviderProps> = ({ clientId, token, children }) => {
12
+ return (
13
+ <QuilttSettingsProvider clientId={clientId}>
14
+ <QuilttAuthProvider token={token}>{children}</QuilttAuthProvider>
15
+ </QuilttSettingsProvider>
16
+ )
17
+ }
18
+
19
+ export default QuilttProvider
@@ -0,0 +1,20 @@
1
+ 'use client'
2
+
3
+ import type { FC, PropsWithChildren } from 'react'
4
+ import { useState } from 'react'
5
+
6
+ import { QuilttSettings } from '../hooks'
7
+
8
+ type QuilttSettingsProviderProps = PropsWithChildren & {
9
+ clientId?: string
10
+ }
11
+
12
+ export const QuilttSettingsProvider: FC<QuilttSettingsProviderProps> = ({ clientId, children }) => {
13
+ const [_clientId] = useState(clientId)
14
+
15
+ return (
16
+ <QuilttSettings.Provider value={{ clientId: _clientId }}>{children}</QuilttSettings.Provider>
17
+ )
18
+ }
19
+
20
+ export default QuilttSettingsProvider
@@ -0,0 +1,3 @@
1
+ export * from './QuilttAuthProvider'
2
+ export * from './QuilttProvider'
3
+ export * from './QuilttSettingsProvider'
package/src/types.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { Component, ComponentType, FC } from 'react'
2
+
3
+ export type AnyTag = string | FC<any> | (new (props: any) => Component)
4
+
5
+ export type PropsOf<Tag> = Tag extends keyof JSX.IntrinsicElements
6
+ ? JSX.IntrinsicElements[Tag]
7
+ : Tag extends ComponentType<infer Props>
8
+ ? Props & JSX.IntrinsicAttributes
9
+ : never
@@ -1,15 +0,0 @@
1
- import { FC, DetailedHTMLProps, ButtonHTMLAttributes, HTMLAttributes } from 'react';
2
-
3
- type QuilttButtonProps = DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> & {
4
- connectorId: string;
5
- connectionId?: string;
6
- };
7
- declare const QuilttButton: FC<QuilttButtonProps>;
8
-
9
- type QuilttContainerProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
10
- connectorId: string;
11
- connectionId?: string;
12
- };
13
- declare const QuilttContainer: FC<QuilttContainerProps>;
14
-
15
- export { QuilttButton as Q, QuilttContainer as a };