@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.
@@ -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
- // Script Element Singleton
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
- // Create Script Element
21
+ // Set Session
15
22
  useEffect(() => {
16
- if (scriptElement) return
23
+ if (typeof Quiltt === 'undefined') return
17
24
 
18
- scriptElement = document.createElement('script')
19
- scriptElement.src = `${QUILTT_CDN_BASE}/v1/connector.js`
25
+ Quiltt.authenticate(session?.token)
26
+ }, [status, session])
20
27
 
21
- if (session?.token) {
22
- scriptElement.setAttribute('quiltt-token', session.token)
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
- document.head.appendChild(scriptElement)
26
- // eslint-disable-next-line react-hooks/exhaustive-deps
27
- }, [])
59
+ connector.onExitSuccess(options.onExitSuccess)
60
+ return () => connector.offExitSuccess(options.onExitSuccess as any)
61
+ }, [connector, options?.onExitSuccess])
28
62
 
29
- // Update Script Element
63
+ // onExitAbort
30
64
  useEffect(() => {
31
- if (!scriptElement) return
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
- 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')
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
- }, [session?.token])
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
+ }