@quiltt/react 2.1.2 → 2.2.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/CHANGELOG.md +80 -0
- package/LICENSE.md +1 -1
- package/README.md +25 -6
- package/dist/{QuilttContainer-3c14e032.d.ts → QuilttContainer-15c62db4.d.ts} +9 -4
- package/dist/components/index.cjs +1 -1
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.ts +2 -1
- package/dist/components/index.js +1 -1
- package/dist/components/index.js.map +1 -1
- package/dist/hooks/index.cjs +1 -1
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/{useStorage-6f448a5a.d.ts → useStorage-d28cfd32.d.ts} +5 -3
- package/package.json +2 -2
- package/src/components/QuilttButton.tsx +18 -3
- package/src/components/QuilttContainer.tsx +20 -2
- package/src/hooks/useQuilttConnector.ts +78 -20
- package/src/hooks/useScript.ts +105 -0
|
@@ -1,39 +1,97 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect } from 'react'
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
4
|
import { useQuilttSession } from './useQuilttSession'
|
|
5
|
+
import { useScript } from './useScript'
|
|
6
|
+
import { ConnectorSDK, ConnectorSDKConnector, ConnectorSDKConnectorOptions } from '@quiltt/core'
|
|
5
7
|
|
|
6
8
|
const QUILTT_CDN_BASE = process.env.QUILTT_CDN_BASE || 'https://cdn.quiltt.io'
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
let scriptElement: HTMLScriptElement
|
|
10
|
+
declare const Quiltt: ConnectorSDK
|
|
10
11
|
|
|
11
|
-
export const useQuilttConnector = (
|
|
12
|
+
export const useQuilttConnector = (
|
|
13
|
+
connectorId?: string,
|
|
14
|
+
options?: ConnectorSDKConnectorOptions
|
|
15
|
+
) => {
|
|
16
|
+
const status = useScript(`${QUILTT_CDN_BASE}/v1/connector.js`)
|
|
12
17
|
const { session } = useQuilttSession()
|
|
18
|
+
const [connector, setConnector] = useState<ConnectorSDKConnector>()
|
|
19
|
+
const [isOpening, setIsOpening] = useState<boolean>(false)
|
|
13
20
|
|
|
14
|
-
//
|
|
21
|
+
// Set Session
|
|
15
22
|
useEffect(() => {
|
|
16
|
-
if (
|
|
23
|
+
if (typeof Quiltt === 'undefined') return
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
Quiltt.authenticate(session?.token)
|
|
26
|
+
}, [status, session])
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
// Set Connector
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (typeof Quiltt === 'undefined' || !connectorId) return
|
|
31
|
+
|
|
32
|
+
if (options?.connectionId) {
|
|
33
|
+
setConnector(Quiltt.reconnect(connectorId, { connectionId: options.connectionId }))
|
|
34
|
+
} else {
|
|
35
|
+
setConnector(Quiltt.connect(connectorId))
|
|
23
36
|
}
|
|
37
|
+
}, [status, connectorId, options?.connectionId])
|
|
38
|
+
|
|
39
|
+
// onEvent
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!connector || !options?.onEvent) return
|
|
42
|
+
|
|
43
|
+
connector.onEvent(options.onEvent)
|
|
44
|
+
return () => connector.offEvent(options.onEvent as any)
|
|
45
|
+
}, [connector, options?.onEvent])
|
|
46
|
+
|
|
47
|
+
// onExit
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!connector || !options?.onExit) return
|
|
50
|
+
|
|
51
|
+
connector.onExit(options.onExit)
|
|
52
|
+
return () => connector.offExit(options.onExit as any)
|
|
53
|
+
}, [connector, options?.onExit])
|
|
54
|
+
|
|
55
|
+
// onExitSuccess
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!connector || !options?.onExitSuccess) return
|
|
24
58
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}, [])
|
|
59
|
+
connector.onExitSuccess(options.onExitSuccess)
|
|
60
|
+
return () => connector.offExitSuccess(options.onExitSuccess as any)
|
|
61
|
+
}, [connector, options?.onExitSuccess])
|
|
28
62
|
|
|
29
|
-
//
|
|
63
|
+
// onExitAbort
|
|
30
64
|
useEffect(() => {
|
|
31
|
-
if (!
|
|
65
|
+
if (!connector || !options?.onExitAbort) return
|
|
66
|
+
|
|
67
|
+
connector.onExitAbort(options.onExitAbort)
|
|
68
|
+
return () => connector.offExitAbort(options.onExitAbort as any)
|
|
69
|
+
}, [connector, options?.onExitAbort])
|
|
70
|
+
|
|
71
|
+
// onExitError
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!connector || !options?.onExitError) return
|
|
74
|
+
|
|
75
|
+
connector.onExitError(options.onExitError)
|
|
76
|
+
return () => connector.offExitError(options.onExitError as any)
|
|
77
|
+
}, [connector, options?.onExitError])
|
|
32
78
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
79
|
+
// This is used to hide any potential race conditions from usage; allowing
|
|
80
|
+
// interaction before the script may have loaded.
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (connector && isOpening) {
|
|
83
|
+
setIsOpening(false)
|
|
84
|
+
connector.open()
|
|
85
|
+
}
|
|
86
|
+
}, [connector, isOpening])
|
|
87
|
+
|
|
88
|
+
const open = useCallback(() => {
|
|
89
|
+
if (connectorId) {
|
|
90
|
+
setIsOpening(true)
|
|
91
|
+
} else {
|
|
92
|
+
throw new Error('Must provide `connectorId` to `open` Quiltt Connector with Method Call')
|
|
37
93
|
}
|
|
38
|
-
}, [
|
|
94
|
+
}, [connectorId, setIsOpening])
|
|
95
|
+
|
|
96
|
+
return { open }
|
|
39
97
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { useEffect, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
export type UseScriptStatus = 'idle' | 'loading' | 'ready' | 'error'
|
|
5
|
+
export interface UseScriptOptions {
|
|
6
|
+
shouldPreventLoad?: boolean
|
|
7
|
+
removeOnUnmount?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Cached script statuses
|
|
11
|
+
const cachedScriptStatuses: Record<string, UseScriptStatus | undefined> = {}
|
|
12
|
+
|
|
13
|
+
function getScriptNode(src: string) {
|
|
14
|
+
const node: HTMLScriptElement | null = document.querySelector(`script[src="${src}"]`)
|
|
15
|
+
const status = node?.getAttribute('data-status') as UseScriptStatus | undefined
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
node,
|
|
19
|
+
status,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// @see https://usehooks-ts.com/react-hook/use-script
|
|
24
|
+
export function useScript(src: string | null, options?: UseScriptOptions): UseScriptStatus {
|
|
25
|
+
const [status, setStatus] = useState<UseScriptStatus>(() => {
|
|
26
|
+
if (!src || options?.shouldPreventLoad) {
|
|
27
|
+
return 'idle'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (typeof window === 'undefined') {
|
|
31
|
+
// SSR Handling - always return 'loading'
|
|
32
|
+
return 'loading'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return cachedScriptStatuses[src] ?? 'loading'
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!src || options?.shouldPreventLoad) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const cachedScriptStatus = cachedScriptStatuses[src]
|
|
44
|
+
if (cachedScriptStatus === 'ready' || cachedScriptStatus === 'error') {
|
|
45
|
+
// If the script is already cached, set its status immediately
|
|
46
|
+
setStatus(cachedScriptStatus)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Fetch existing script element by src
|
|
51
|
+
// It may have been added by another instance of this hook
|
|
52
|
+
const script = getScriptNode(src)
|
|
53
|
+
let scriptNode = script.node
|
|
54
|
+
|
|
55
|
+
if (!scriptNode) {
|
|
56
|
+
// Create script element and add it to document body
|
|
57
|
+
scriptNode = document.createElement('script')
|
|
58
|
+
scriptNode.src = src
|
|
59
|
+
scriptNode.async = true
|
|
60
|
+
scriptNode.setAttribute('data-status', 'loading')
|
|
61
|
+
document.body.appendChild(scriptNode)
|
|
62
|
+
|
|
63
|
+
// Store status in attribute on script
|
|
64
|
+
// This can be read by other instances of this hook
|
|
65
|
+
const setAttributeFromEvent = (event: Event) => {
|
|
66
|
+
const scriptStatus: UseScriptStatus = event.type === 'load' ? 'ready' : 'error'
|
|
67
|
+
|
|
68
|
+
scriptNode?.setAttribute('data-status', scriptStatus)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
scriptNode.addEventListener('load', setAttributeFromEvent)
|
|
72
|
+
scriptNode.addEventListener('error', setAttributeFromEvent)
|
|
73
|
+
} else {
|
|
74
|
+
// Grab existing script status from attribute and set to state.
|
|
75
|
+
setStatus(script.status ?? cachedScriptStatus ?? 'loading')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Script event handler to update status in state
|
|
79
|
+
// Note: Even if the script already exists we still need to add
|
|
80
|
+
// event handlers to update the state for *this* hook instance.
|
|
81
|
+
const setStateFromEvent = (event: Event) => {
|
|
82
|
+
const newStatus = event.type === 'load' ? 'ready' : 'error'
|
|
83
|
+
setStatus(newStatus)
|
|
84
|
+
cachedScriptStatuses[src] = newStatus
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Add event listeners
|
|
88
|
+
scriptNode.addEventListener('load', setStateFromEvent)
|
|
89
|
+
scriptNode.addEventListener('error', setStateFromEvent)
|
|
90
|
+
|
|
91
|
+
// Remove event listeners on cleanup
|
|
92
|
+
return () => {
|
|
93
|
+
if (scriptNode) {
|
|
94
|
+
scriptNode.removeEventListener('load', setStateFromEvent)
|
|
95
|
+
scriptNode.removeEventListener('error', setStateFromEvent)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (scriptNode && options?.removeOnUnmount) {
|
|
99
|
+
scriptNode.remove()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}, [src, options?.shouldPreventLoad, options?.removeOnUnmount])
|
|
103
|
+
|
|
104
|
+
return status
|
|
105
|
+
}
|