@quiltt/react 4.0.1 → 4.2.0

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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # @quiltt/react
2
2
 
3
+ ## 4.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#348](https://github.com/quiltt/quiltt-js/pull/348) [`7e27845`](https://github.com/quiltt/quiltt-js/commit/7e2784523124c87fc6654d8336f924286daade1b) Thanks [@zubairaziz](https://github.com/zubairaziz)! - resolve connectionId persistence issue in QuilttContainer
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [[`7e27845`](https://github.com/quiltt/quiltt-js/commit/7e2784523124c87fc6654d8336f924286daade1b)]:
12
+ - @quiltt/core@4.2.0
13
+
14
+ ## 4.1.0
15
+
16
+ ### Minor Changes
17
+
18
+ - [#342](https://github.com/quiltt/quiltt-js/pull/342) [`6a387ba`](https://github.com/quiltt/quiltt-js/commit/6a387ba0db77912df85c6cd1924f63edf50f9cdd) Thanks [@sirwolfgang](https://github.com/sirwolfgang)! - Create useQuilttInstitutions hook
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies [[`6a387ba`](https://github.com/quiltt/quiltt-js/commit/6a387ba0db77912df85c6cd1924f63edf50f9cdd)]:
23
+ - @quiltt/core@4.1.0
24
+
3
25
  ## 4.0.1
4
26
 
5
27
  ### Patch Changes
@@ -6,7 +6,7 @@ import './QuilttSettings-client-BK-0SQME.js';
6
6
  import './useSession-client-CCAvnROP.js';
7
7
  import { jsx } from 'react/jsx-runtime';
8
8
  import { ApolloProvider } from '@apollo/client/react/context/ApolloProvider.js';
9
- import { u as useQuilttSession } from './useQuilttSession-client-D2mjVT4S.js';
9
+ import { u as useQuilttSession } from './useQuilttSession-client-Ddb55W0n.js';
10
10
 
11
11
  const useIdentifySession = (auth, setSession)=>{
12
12
  const identifySession = useCallback(async (payload, callbacks)=>{
@@ -187,4 +187,4 @@ const useRevokeSession = (auth, session, setSession)=>{
187
187
  });
188
188
  };
189
189
 
190
- export { QuilttAuthProvider as Q, useIdentifySession as a, useAuthenticateSession as b, useRevokeSession as c, useImportSession as u };
190
+ export { QuilttAuthProvider as Q, useIdentifySession as a, useAuthenticateSession as b, useRevokeSession as c, isDeepEqual as i, useImportSession as u };
package/dist/index.d.ts CHANGED
@@ -133,6 +133,12 @@ type BaseQuilttButtonProps<T extends ElementType> = {
133
133
  connectorId: string;
134
134
  connectionId?: string;
135
135
  institution?: string;
136
+ /**
137
+ * Forces complete remount when connectionId changes.
138
+ * Useful as a fallback for ensuring clean state.
139
+ * @default false
140
+ */
141
+ forceRemountOnConnectionChange?: boolean;
136
142
  onClick?: (event: MouseEvent<HTMLElement>) => void;
137
143
  };
138
144
  type QuilttCallbackProps = Omit<ConnectorSDKCallbacks, 'onLoad'> & {
@@ -140,18 +146,35 @@ type QuilttCallbackProps = Omit<ConnectorSDKCallbacks, 'onLoad'> & {
140
146
  onHtmlLoad?: React.ReactEventHandler<HTMLElement>;
141
147
  };
142
148
  type QuilttButtonProps<T extends ElementType> = PropsWithChildren<BaseQuilttButtonProps<T> & QuilttCallbackProps>;
143
- declare const QuilttButton: <T extends ElementType = "button">({ as, connectorId, connectionId, institution, onEvent, onOpen, onLoad, onExit, onExitSuccess, onExitAbort, onExitError, onClick, onHtmlLoad, children, ...props }: QuilttButtonProps<T> & PropsOf<T>) => react_jsx_runtime.JSX.Element;
149
+ /**
150
+ * QuilttButton provides a clickable interface to open Quiltt connectors.
151
+ *
152
+ * When connectionId changes, the button will automatically update the existing
153
+ * connector instance with the new connection details. If you need to force a
154
+ * complete remount instead, set forceRemountOnConnectionChange to true.
155
+ */
156
+ declare const QuilttButton: <T extends ElementType = "button">({ as, connectorId, connectionId, institution, forceRemountOnConnectionChange, onEvent, onOpen, onLoad, onExit, onExitSuccess, onExitAbort, onExitError, onClick, onHtmlLoad, children, ...props }: QuilttButtonProps<T> & PropsOf<T>) => react_jsx_runtime.JSX.Element;
144
157
 
145
158
  type QuilttContainerProps<T extends ElementType> = PropsWithChildren<{
146
159
  as?: T;
147
160
  connectorId: string;
148
161
  connectionId?: string;
162
+ /**
163
+ * Forces complete remount when connectionId changes.
164
+ * Useful as a fallback for ensuring clean state.
165
+ * @default false
166
+ */
167
+ forceRemountOnConnectionChange?: boolean;
149
168
  } & ConnectorSDKCallbacks>;
150
169
  /**
151
170
  * QuilttContainer uses globally shared callbacks. It's recommended you only use
152
171
  * one Container at a time.
172
+ *
173
+ * When connectionId changes, the container will automatically update the existing
174
+ * connector instance with the new connection details. If you need to force a
175
+ * complete remount instead, set forceRemountOnConnectionChange to true.
153
176
  */
154
- declare const QuilttContainer: <T extends ElementType = "div">({ as, connectorId, connectionId, onEvent, onLoad, onExit, onExitSuccess, onExitAbort, onExitError, children, ...props }: QuilttContainerProps<T> & PropsOf<T>) => react_jsx_runtime.JSX.Element;
177
+ declare const QuilttContainer: <T extends ElementType = "div">({ as, connectorId, connectionId, forceRemountOnConnectionChange, onEvent, onLoad, onExit, onExitSuccess, onExitAbort, onExitError, children, ...props }: QuilttContainerProps<T> & PropsOf<T>) => react_jsx_runtime.JSX.Element;
155
178
 
156
179
  export { QuilttAuthProvider, QuilttButton, QuilttContainer, QuilttProvider, QuilttSettingsProvider, useAuthenticateSession, useEventListener, useIdentifySession, useImportSession, useIsomorphicLayoutEffect, useQuilttClient, useQuilttConnector, useQuilttSession, useQuilttSettings, useRevokeSession, useSession, useStorage };
157
180
  export type { AuthenticateSession, IdentifySession, ImportSession, RevokeSession, SetSession, UseQuilttSession };
package/dist/index.js CHANGED
@@ -1,16 +1,17 @@
1
1
  export * from '@quiltt/core';
2
2
  export { u as useEventListener } from './useEventListener-client-DVM5xwKY.js';
3
3
  export { u as useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect-client-DeTHOKz1.js';
4
- import { Q as QuilttAuthProvider } from './QuilttAuthProvider-client-CER-TOln.js';
5
- export { b as useAuthenticateSession, a as useIdentifySession, u as useImportSession, c as useRevokeSession } from './QuilttAuthProvider-client-CER-TOln.js';
4
+ import { Q as QuilttAuthProvider, i as isDeepEqual } from './QuilttAuthProvider-client-DLARZukU.js';
5
+ export { b as useAuthenticateSession, a as useIdentifySession, u as useImportSession, c as useRevokeSession } from './QuilttAuthProvider-client-DLARZukU.js';
6
6
  export { u as useQuilttClient } from './useQuilttClient-client-CAAUait1.js';
7
- import { u as useQuilttConnector } from './useQuilttConnector-client-sRplTzek.js';
8
- export { u as useQuilttSession } from './useQuilttSession-client-D2mjVT4S.js';
7
+ import { u as useQuilttConnector } from './useQuilttConnector-client-BK7ybRZe.js';
8
+ export { u as useQuilttSession } from './useQuilttSession-client-Ddb55W0n.js';
9
9
  export { u as useQuilttSettings } from './useQuilttSettings-client-BOCBjFXe.js';
10
10
  export { u as useSession } from './useSession-client-CCAvnROP.js';
11
11
  export { u as useStorage } from './useStorage-client-DHcq3Kuh.js';
12
12
  import { jsx } from 'react/jsx-runtime';
13
13
  import { Q as QuilttSettingsProvider } from './QuilttSettingsProvider-client-Va7uJ_dQ.js';
14
+ import { useRef, useEffect } from 'react';
14
15
 
15
16
  const QuilttProvider = ({ clientId, token, children })=>{
16
17
  return /*#__PURE__*/ jsx(QuilttSettingsProvider, {
@@ -22,7 +23,46 @@ const QuilttProvider = ({ clientId, token, children })=>{
22
23
  });
23
24
  };
24
25
 
25
- const QuilttButton = ({ as, connectorId, connectionId, institution, onEvent, onOpen, onLoad, onExit, onExitSuccess, onExitAbort, onExitError, onClick, onHtmlLoad, children, ...props })=>{
26
+ /**
27
+ * QuilttButton provides a clickable interface to open Quiltt connectors.
28
+ *
29
+ * When connectionId changes, the button will automatically update the existing
30
+ * connector instance with the new connection details. If you need to force a
31
+ * complete remount instead, set forceRemountOnConnectionChange to true.
32
+ */ const QuilttButton = ({ as, connectorId, connectionId, institution, forceRemountOnConnectionChange = false, onEvent, onOpen, onLoad, onExit, onExitSuccess, onExitAbort, onExitError, onClick, onHtmlLoad, children, ...props })=>{
33
+ // Keep track of previous connectionId for change detection
34
+ const prevConnectionIdRef = useRef(connectionId);
35
+ const prevCallbacksRef = useRef({
36
+ onEvent,
37
+ onOpen,
38
+ onLoad,
39
+ onExit,
40
+ onExitSuccess,
41
+ onExitAbort,
42
+ onExitError
43
+ });
44
+ // Track if callbacks have changed to help with debugging
45
+ const currentCallbacks = {
46
+ onEvent,
47
+ onOpen,
48
+ onLoad,
49
+ onExit,
50
+ onExitSuccess,
51
+ onExitAbort,
52
+ onExitError
53
+ };
54
+ const callbacksChanged = !isDeepEqual(prevCallbacksRef.current, currentCallbacks);
55
+ useEffect(()=>{
56
+ prevCallbacksRef.current = currentCallbacks;
57
+ });
58
+ // Warning for potential callback reference issues
59
+ useEffect(()=>{
60
+ if (callbacksChanged && prevConnectionIdRef.current !== undefined) {
61
+ console.warn('[Quiltt] Callback functions changed after initial render. ' + 'This may cause unexpected behavior. Consider memoizing callback functions ' + 'with useCallback to maintain stable references.');
62
+ }
63
+ }, [
64
+ callbacksChanged
65
+ ]);
26
66
  const { open } = useQuilttConnector(connectorId, {
27
67
  connectionId,
28
68
  institution,
@@ -35,6 +75,12 @@ const QuilttButton = ({ as, connectorId, connectionId, institution, onEvent, onO
35
75
  onExitAbort,
36
76
  onExitError
37
77
  });
78
+ // Update previous connectionId reference
79
+ useEffect(()=>{
80
+ prevConnectionIdRef.current = connectionId;
81
+ }, [
82
+ connectionId
83
+ ]);
38
84
  const Button = as || 'button';
39
85
  const handleClick = (event)=>{
40
86
  // Call the user's onClick handler first to allow for:
@@ -45,20 +91,58 @@ const QuilttButton = ({ as, connectorId, connectionId, institution, onEvent, onO
45
91
  // Then open the Quiltt connector
46
92
  open();
47
93
  };
94
+ // Generate key for forced remounting if enabled, but respect user-provided key
95
+ const buttonKey = props.key ?? (forceRemountOnConnectionChange ? `${connectorId}-${connectionId || 'no-connection'}` : undefined);
48
96
  return /*#__PURE__*/ jsx(Button, {
49
97
  onClick: handleClick,
50
98
  onLoad: onHtmlLoad,
51
99
  "quiltt-connection": connectionId,
52
100
  ...props,
53
101
  children: children
54
- });
102
+ }, buttonKey);
55
103
  };
56
104
 
57
105
  /**
58
106
  * QuilttContainer uses globally shared callbacks. It's recommended you only use
59
107
  * one Container at a time.
60
- */ const QuilttContainer = ({ as, connectorId, connectionId, onEvent, onLoad, onExit, onExitSuccess, onExitAbort, onExitError, children, ...props })=>{
108
+ *
109
+ * When connectionId changes, the container will automatically update the existing
110
+ * connector instance with the new connection details. If you need to force a
111
+ * complete remount instead, set forceRemountOnConnectionChange to true.
112
+ */ const QuilttContainer = ({ as, connectorId, connectionId, forceRemountOnConnectionChange = false, onEvent, onLoad, onExit, onExitSuccess, onExitAbort, onExitError, children, ...props })=>{
113
+ // Keep track of previous connectionId for change detection
114
+ const prevConnectionIdRef = useRef(connectionId);
115
+ const prevCallbacksRef = useRef({
116
+ onEvent,
117
+ onLoad,
118
+ onExit,
119
+ onExitSuccess,
120
+ onExitAbort,
121
+ onExitError
122
+ });
123
+ // Track if callbacks have changed to help with debugging
124
+ const currentCallbacks = {
125
+ onEvent,
126
+ onLoad,
127
+ onExit,
128
+ onExitSuccess,
129
+ onExitAbort,
130
+ onExitError
131
+ };
132
+ const callbacksChanged = !isDeepEqual(prevCallbacksRef.current, currentCallbacks);
133
+ useEffect(()=>{
134
+ prevCallbacksRef.current = currentCallbacks;
135
+ });
136
+ // Warning for potential callback reference issues
137
+ useEffect(()=>{
138
+ if (callbacksChanged && prevConnectionIdRef.current !== undefined) {
139
+ console.warn('[Quiltt] Callback functions changed after initial render. ' + 'This may cause unexpected behavior. Consider memoizing callback functions ' + 'with useCallback to maintain stable references.');
140
+ }
141
+ }, [
142
+ callbacksChanged
143
+ ]);
61
144
  useQuilttConnector(connectorId, {
145
+ connectionId,
62
146
  nonce: props?.nonce,
63
147
  onEvent,
64
148
  onLoad,
@@ -67,13 +151,21 @@ const QuilttButton = ({ as, connectorId, connectionId, institution, onEvent, onO
67
151
  onExitAbort,
68
152
  onExitError
69
153
  });
154
+ // Update previous connectionId reference
155
+ useEffect(()=>{
156
+ prevConnectionIdRef.current = connectionId;
157
+ }, [
158
+ connectionId
159
+ ]);
70
160
  const Container = as || 'div';
161
+ // Generate key for forced remounting if enabled, but respect user-provided key
162
+ const containerKey = props.key ?? (forceRemountOnConnectionChange ? `${connectorId}-${connectionId || 'no-connection'}` : undefined);
71
163
  return /*#__PURE__*/ jsx(Container, {
72
164
  "quiltt-container": connectorId,
73
165
  "quiltt-connection": connectionId,
74
166
  ...props,
75
167
  children: children
76
- });
168
+ }, containerKey);
77
169
  };
78
170
 
79
171
  export { QuilttAuthProvider, QuilttButton, QuilttContainer, QuilttProvider, QuilttSettingsProvider, useQuilttConnector };
@@ -1,45 +1,74 @@
1
1
  'use client';
2
- import { useState, useEffect, useCallback } from 'react';
2
+ import { useState, useRef, useEffect, useCallback } from 'react';
3
3
  import { cdnBase } from '@quiltt/core';
4
- import { u as useQuilttSession } from './useQuilttSession-client-D2mjVT4S.js';
4
+ import { u as useQuilttSession } from './useQuilttSession-client-Ddb55W0n.js';
5
5
  import { u as useScript } from './useScript-client-JCgaTW9n.js';
6
6
 
7
- var version = "4.0.1";
7
+ var version = "4.2.0";
8
8
 
9
9
  const useQuilttConnector = (connectorId, options)=>{
10
10
  const status = useScript(`${cdnBase}/v1/connector.js?agent=react-${version}`, {
11
11
  nonce: options?.nonce
12
12
  });
13
- const { session } = useQuilttSession();
13
+ // This ensures we're not destructuring `session` before the script has loaded
14
+ const useQuilttSessionReturn = useQuilttSession();
15
+ const session = useQuilttSessionReturn?.session || null;
14
16
  const [connector, setConnector] = useState();
15
17
  const [isOpening, setIsOpening] = useState(false);
18
+ // Keep track of the previous connectionId to detect changes
19
+ const prevConnectionIdRef = useRef(options?.connectionId);
20
+ const prevConnectorIdRef = useRef(connectorId);
21
+ const connectorCreatedRef = useRef(false);
16
22
  // Set Session
17
- // biome-ignore lint/correctness/useExhaustiveDependencies: We also need to update on status change
18
23
  useEffect(()=>{
19
24
  if (typeof Quiltt === 'undefined') return;
25
+ console.debug('[Quiltt] script status: ', status);
20
26
  Quiltt.authenticate(session?.token);
21
27
  }, [
22
28
  status,
23
29
  session?.token
24
30
  ]);
25
31
  // Set Connector
26
- // biome-ignore lint/correctness/useExhaustiveDependencies: We also need to update on status change
27
32
  useEffect(()=>{
28
33
  if (typeof Quiltt === 'undefined' || !connectorId) return;
29
- if (options?.connectionId) {
30
- setConnector(Quiltt.reconnect(connectorId, {
31
- connectionId: options.connectionId
32
- }));
33
- } else {
34
- setConnector(Quiltt.connect(connectorId, {
35
- institution: options?.institution
36
- }));
34
+ console.debug('[Quiltt] script status: ', status);
35
+ const currentConnectionId = options?.connectionId;
36
+ const currentInstitution = options?.institution;
37
+ // Check if this is a connectionId change on the same connector
38
+ const connectionIdChanged = prevConnectionIdRef.current !== currentConnectionId;
39
+ const connectorIdChanged = prevConnectorIdRef.current !== connectorId;
40
+ // If only connectionId changed (not the connectorId), the core SDK should handle this
41
+ // via the updated Handler.updateOptions method, so we don't need to recreate the connector
42
+ if (connectionIdChanged && !connectorIdChanged && connectorCreatedRef.current) {
43
+ // The SDK will automatically update the existing handler with new connectionId
44
+ // via the DocumentObserver -> Engine.onChange -> Handler.updateOptions flow
45
+ console.debug('[Quiltt] connectionId changed, SDK will handle update automatically');
46
+ // Update our refs
47
+ prevConnectionIdRef.current = currentConnectionId;
48
+ return;
49
+ }
50
+ // Only create new connector if we haven't created one yet or if connectorId changed
51
+ if (!connectorCreatedRef.current || connectorIdChanged) {
52
+ // Create new connector (initial mount or connectorId changed)
53
+ if (currentConnectionId) {
54
+ setConnector(Quiltt.reconnect(connectorId, {
55
+ connectionId: currentConnectionId
56
+ }));
57
+ } else {
58
+ setConnector(Quiltt.connect(connectorId, {
59
+ institution: currentInstitution
60
+ }));
61
+ }
62
+ connectorCreatedRef.current = true;
37
63
  }
64
+ // Update refs
65
+ prevConnectionIdRef.current = currentConnectionId;
66
+ prevConnectorIdRef.current = connectorId;
38
67
  }, [
39
- status,
40
68
  connectorId,
41
69
  options?.connectionId,
42
- options?.institution
70
+ options?.institution,
71
+ status
43
72
  ]);
44
73
  // onEvent
45
74
  useEffect(()=>{
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { useCallback } from 'react';
3
3
  import { AuthAPI } from '@quiltt/core';
4
- import { u as useImportSession, a as useIdentifySession, b as useAuthenticateSession, c as useRevokeSession } from './QuilttAuthProvider-client-CER-TOln.js';
4
+ import { u as useImportSession, a as useIdentifySession, b as useAuthenticateSession, c as useRevokeSession } from './QuilttAuthProvider-client-DLARZukU.js';
5
5
  import { u as useQuilttSettings } from './useQuilttSettings-client-BOCBjFXe.js';
6
6
  import { u as useSession } from './useSession-client-CCAvnROP.js';
7
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quiltt/react",
3
- "version": "4.0.1",
3
+ "version": "4.2.0",
4
4
  "description": "React Components and Hooks for Quiltt Connector",
5
5
  "keywords": [
6
6
  "quiltt",
@@ -35,7 +35,8 @@
35
35
  "main": "dist/index.js",
36
36
  "dependencies": {
37
37
  "@apollo/client": "^3.12.4",
38
- "@quiltt/core": "4.0.1"
38
+ "use-debounce": "^10.0.4",
39
+ "@quiltt/core": "4.2.0"
39
40
  },
40
41
  "devDependencies": {
41
42
  "@biomejs/biome": "1.9.4",
@@ -1,9 +1,11 @@
1
+ import { useEffect, useRef } from 'react'
1
2
  import type { ElementType, MouseEvent, PropsWithChildren } from 'react'
2
3
 
3
4
  import type { ConnectorSDKCallbacks } from '@quiltt/core'
4
5
 
5
6
  import { useQuilttConnector } from '@/hooks/useQuilttConnector'
6
7
  import type { PropsOf } from '@/types'
8
+ import { isDeepEqual } from '@/utils/isDeepEqual'
7
9
 
8
10
  // Base button props without callback-specific properties
9
11
  type BaseQuilttButtonProps<T extends ElementType> = {
@@ -11,6 +13,14 @@ type BaseQuilttButtonProps<T extends ElementType> = {
11
13
  connectorId: string
12
14
  connectionId?: string // For Reconnect Mode
13
15
  institution?: string // For Connect Mode
16
+
17
+ /**
18
+ * Forces complete remount when connectionId changes.
19
+ * Useful as a fallback for ensuring clean state.
20
+ * @default false
21
+ */
22
+ forceRemountOnConnectionChange?: boolean
23
+
14
24
  // Override the native onClick handler
15
25
  onClick?: (event: MouseEvent<HTMLElement>) => void
16
26
  }
@@ -27,11 +37,19 @@ type QuilttButtonProps<T extends ElementType> = PropsWithChildren<
27
37
  BaseQuilttButtonProps<T> & QuilttCallbackProps
28
38
  >
29
39
 
40
+ /**
41
+ * QuilttButton provides a clickable interface to open Quiltt connectors.
42
+ *
43
+ * When connectionId changes, the button will automatically update the existing
44
+ * connector instance with the new connection details. If you need to force a
45
+ * complete remount instead, set forceRemountOnConnectionChange to true.
46
+ */
30
47
  export const QuilttButton = <T extends ElementType = 'button'>({
31
48
  as,
32
49
  connectorId,
33
50
  connectionId,
34
51
  institution,
52
+ forceRemountOnConnectionChange = false,
35
53
  onEvent,
36
54
  onOpen,
37
55
  onLoad,
@@ -44,6 +62,46 @@ export const QuilttButton = <T extends ElementType = 'button'>({
44
62
  children,
45
63
  ...props
46
64
  }: QuilttButtonProps<T> & PropsOf<T>) => {
65
+ // Keep track of previous connectionId for change detection
66
+ const prevConnectionIdRef = useRef<string | undefined>(connectionId)
67
+ const prevCallbacksRef = useRef({
68
+ onEvent,
69
+ onOpen,
70
+ onLoad,
71
+ onExit,
72
+ onExitSuccess,
73
+ onExitAbort,
74
+ onExitError,
75
+ })
76
+
77
+ // Track if callbacks have changed to help with debugging
78
+ const currentCallbacks = {
79
+ onEvent,
80
+ onOpen,
81
+ onLoad,
82
+ onExit,
83
+ onExitSuccess,
84
+ onExitAbort,
85
+ onExitError,
86
+ }
87
+
88
+ const callbacksChanged = !isDeepEqual(prevCallbacksRef.current, currentCallbacks)
89
+
90
+ useEffect(() => {
91
+ prevCallbacksRef.current = currentCallbacks
92
+ })
93
+
94
+ // Warning for potential callback reference issues
95
+ useEffect(() => {
96
+ if (callbacksChanged && prevConnectionIdRef.current !== undefined) {
97
+ console.warn(
98
+ '[Quiltt] Callback functions changed after initial render. ' +
99
+ 'This may cause unexpected behavior. Consider memoizing callback functions ' +
100
+ 'with useCallback to maintain stable references.'
101
+ )
102
+ }
103
+ }, [callbacksChanged])
104
+
47
105
  const { open } = useQuilttConnector(connectorId, {
48
106
  connectionId,
49
107
  institution,
@@ -57,6 +115,11 @@ export const QuilttButton = <T extends ElementType = 'button'>({
57
115
  onExitError,
58
116
  })
59
117
 
118
+ // Update previous connectionId reference
119
+ useEffect(() => {
120
+ prevConnectionIdRef.current = connectionId
121
+ }, [connectionId])
122
+
60
123
  const Button = as || 'button'
61
124
 
62
125
  const handleClick = (event: MouseEvent<HTMLElement>) => {
@@ -70,8 +133,21 @@ export const QuilttButton = <T extends ElementType = 'button'>({
70
133
  open()
71
134
  }
72
135
 
136
+ // Generate key for forced remounting if enabled, but respect user-provided key
137
+ const buttonKey =
138
+ props.key ??
139
+ (forceRemountOnConnectionChange
140
+ ? `${connectorId}-${connectionId || 'no-connection'}`
141
+ : undefined)
142
+
73
143
  return (
74
- <Button onClick={handleClick} onLoad={onHtmlLoad} quiltt-connection={connectionId} {...props}>
144
+ <Button
145
+ key={buttonKey}
146
+ onClick={handleClick}
147
+ onLoad={onHtmlLoad}
148
+ quiltt-connection={connectionId}
149
+ {...props}
150
+ >
75
151
  {children}
76
152
  </Button>
77
153
  )
@@ -1,26 +1,40 @@
1
+ import { useEffect, useRef } from 'react'
1
2
  import type { ElementType, PropsWithChildren } from 'react'
2
3
 
3
4
  import type { ConnectorSDKCallbacks } from '@quiltt/core'
4
5
 
5
6
  import { useQuilttConnector } from '@/hooks/useQuilttConnector'
6
7
  import type { PropsOf } from '@/types'
8
+ import { isDeepEqual } from '@/utils/isDeepEqual'
7
9
 
8
10
  type QuilttContainerProps<T extends ElementType> = PropsWithChildren<
9
11
  {
10
12
  as?: T
11
13
  connectorId: string
12
14
  connectionId?: string // For Reconnect Mode
15
+
16
+ /**
17
+ * Forces complete remount when connectionId changes.
18
+ * Useful as a fallback for ensuring clean state.
19
+ * @default false
20
+ */
21
+ forceRemountOnConnectionChange?: boolean
13
22
  } & ConnectorSDKCallbacks
14
23
  >
15
24
 
16
25
  /**
17
26
  * QuilttContainer uses globally shared callbacks. It's recommended you only use
18
27
  * one Container at a time.
28
+ *
29
+ * When connectionId changes, the container will automatically update the existing
30
+ * connector instance with the new connection details. If you need to force a
31
+ * complete remount instead, set forceRemountOnConnectionChange to true.
19
32
  */
20
33
  export const QuilttContainer = <T extends ElementType = 'div'>({
21
34
  as,
22
35
  connectorId,
23
36
  connectionId,
37
+ forceRemountOnConnectionChange = false,
24
38
  onEvent,
25
39
  onLoad,
26
40
  onExit,
@@ -30,7 +44,46 @@ export const QuilttContainer = <T extends ElementType = 'div'>({
30
44
  children,
31
45
  ...props
32
46
  }: QuilttContainerProps<T> & PropsOf<T>) => {
47
+ // Keep track of previous connectionId for change detection
48
+ const prevConnectionIdRef = useRef<string | undefined>(connectionId)
49
+ const prevCallbacksRef = useRef({
50
+ onEvent,
51
+ onLoad,
52
+ onExit,
53
+ onExitSuccess,
54
+ onExitAbort,
55
+ onExitError,
56
+ })
57
+
58
+ // Track if callbacks have changed to help with debugging
59
+ const currentCallbacks = {
60
+ onEvent,
61
+ onLoad,
62
+ onExit,
63
+ onExitSuccess,
64
+ onExitAbort,
65
+ onExitError,
66
+ }
67
+
68
+ const callbacksChanged = !isDeepEqual(prevCallbacksRef.current, currentCallbacks)
69
+
70
+ useEffect(() => {
71
+ prevCallbacksRef.current = currentCallbacks
72
+ })
73
+
74
+ // Warning for potential callback reference issues
75
+ useEffect(() => {
76
+ if (callbacksChanged && prevConnectionIdRef.current !== undefined) {
77
+ console.warn(
78
+ '[Quiltt] Callback functions changed after initial render. ' +
79
+ 'This may cause unexpected behavior. Consider memoizing callback functions ' +
80
+ 'with useCallback to maintain stable references.'
81
+ )
82
+ }
83
+ }, [callbacksChanged])
84
+
33
85
  useQuilttConnector(connectorId, {
86
+ connectionId,
34
87
  nonce: props?.nonce, // Pass nonce for script loading if needed
35
88
  onEvent,
36
89
  onLoad,
@@ -40,10 +93,27 @@ export const QuilttContainer = <T extends ElementType = 'div'>({
40
93
  onExitError,
41
94
  })
42
95
 
96
+ // Update previous connectionId reference
97
+ useEffect(() => {
98
+ prevConnectionIdRef.current = connectionId
99
+ }, [connectionId])
100
+
43
101
  const Container = as || 'div'
44
102
 
103
+ // Generate key for forced remounting if enabled, but respect user-provided key
104
+ const containerKey =
105
+ props.key ??
106
+ (forceRemountOnConnectionChange
107
+ ? `${connectorId}-${connectionId || 'no-connection'}`
108
+ : undefined)
109
+
45
110
  return (
46
- <Container quiltt-container={connectorId} quiltt-connection={connectionId} {...props}>
111
+ <Container
112
+ key={containerKey}
113
+ quiltt-container={connectorId}
114
+ quiltt-connection={connectionId}
115
+ {...props}
116
+ >
47
117
  {children}
48
118
  </Container>
49
119
  )
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useCallback, useEffect, useState } from 'react'
3
+ import { useCallback, useEffect, useRef, useState } from 'react'
4
4
 
5
5
  import { cdnBase } from '@quiltt/core'
6
6
  import type {
@@ -22,29 +22,67 @@ export const useQuilttConnector = (
22
22
  const status = useScript(`${cdnBase}/v1/connector.js?agent=react-${version}`, {
23
23
  nonce: options?.nonce,
24
24
  })
25
- const { session } = useQuilttSession()
25
+
26
+ // This ensures we're not destructuring `session` before the script has loaded
27
+ const useQuilttSessionReturn = useQuilttSession()
28
+ const session = useQuilttSessionReturn?.session || null
29
+
26
30
  const [connector, setConnector] = useState<ConnectorSDKConnector>()
27
31
  const [isOpening, setIsOpening] = useState<boolean>(false)
28
32
 
33
+ // Keep track of the previous connectionId to detect changes
34
+ const prevConnectionIdRef = useRef<string | undefined>(options?.connectionId)
35
+ const prevConnectorIdRef = useRef<string | undefined>(connectorId)
36
+ const connectorCreatedRef = useRef<boolean>(false)
37
+
29
38
  // Set Session
30
- // biome-ignore lint/correctness/useExhaustiveDependencies: We also need to update on status change
31
39
  useEffect(() => {
32
40
  if (typeof Quiltt === 'undefined') return
41
+ console.debug('[Quiltt] script status: ', status)
33
42
 
34
43
  Quiltt.authenticate(session?.token)
35
44
  }, [status, session?.token])
36
45
 
37
46
  // Set Connector
38
- // biome-ignore lint/correctness/useExhaustiveDependencies: We also need to update on status change
39
47
  useEffect(() => {
40
48
  if (typeof Quiltt === 'undefined' || !connectorId) return
49
+ console.debug('[Quiltt] script status: ', status)
41
50
 
42
- if (options?.connectionId) {
43
- setConnector(Quiltt.reconnect(connectorId, { connectionId: options.connectionId }))
44
- } else {
45
- setConnector(Quiltt.connect(connectorId, { institution: options?.institution }))
51
+ const currentConnectionId = options?.connectionId
52
+ const currentInstitution = options?.institution
53
+
54
+ // Check if this is a connectionId change on the same connector
55
+ const connectionIdChanged = prevConnectionIdRef.current !== currentConnectionId
56
+ const connectorIdChanged = prevConnectorIdRef.current !== connectorId
57
+
58
+ // If only connectionId changed (not the connectorId), the core SDK should handle this
59
+ // via the updated Handler.updateOptions method, so we don't need to recreate the connector
60
+ if (connectionIdChanged && !connectorIdChanged && connectorCreatedRef.current) {
61
+ // The SDK will automatically update the existing handler with new connectionId
62
+ // via the DocumentObserver -> Engine.onChange -> Handler.updateOptions flow
63
+ console.debug('[Quiltt] connectionId changed, SDK will handle update automatically')
64
+
65
+ // Update our refs
66
+ prevConnectionIdRef.current = currentConnectionId
67
+ return
46
68
  }
47
- }, [status, connectorId, options?.connectionId, options?.institution])
69
+
70
+ // Only create new connector if we haven't created one yet or if connectorId changed
71
+ if (!connectorCreatedRef.current || connectorIdChanged) {
72
+ // Create new connector (initial mount or connectorId changed)
73
+ if (currentConnectionId) {
74
+ setConnector(Quiltt.reconnect(connectorId, { connectionId: currentConnectionId }))
75
+ } else {
76
+ setConnector(Quiltt.connect(connectorId, { institution: currentInstitution }))
77
+ }
78
+
79
+ connectorCreatedRef.current = true
80
+ }
81
+
82
+ // Update refs
83
+ prevConnectionIdRef.current = currentConnectionId
84
+ prevConnectorIdRef.current = connectorId
85
+ }, [connectorId, options?.connectionId, options?.institution, status])
48
86
 
49
87
  // onEvent
50
88
  useEffect(() => {
@@ -0,0 +1,124 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useMemo, useState } from 'react'
4
+
5
+ import { useDebounce } from 'use-debounce'
6
+
7
+ import { InstitutionsAPI } from '@quiltt/core'
8
+ import type { ErrorData, InstitutionsData } from '@quiltt/core'
9
+
10
+ import { version } from '@/version'
11
+ import useSession from './useSession'
12
+
13
+ export type UseQuilttInstitutions = (
14
+ connectorId: string,
15
+ onErrorCallback?: (msg: string) => void
16
+ ) => {
17
+ searchTerm: string
18
+ searchResults: InstitutionsData
19
+ isSearching: boolean
20
+ setSearchTerm: (term: string) => void
21
+ }
22
+
23
+ export const useQuilttInstitutions: UseQuilttInstitutions = (connectorId, onErrorCallback) => {
24
+ const agent = useMemo(() => {
25
+ // Try deprecated navigator.product first (still used in some RN versions)
26
+ if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
27
+ return `react-native-${version}`
28
+ }
29
+
30
+ // Detect React Native by its unique environment characteristics
31
+ const isReactNative = !!(
32
+ // Has window (unlike Node.js)
33
+ (
34
+ typeof window !== 'undefined' &&
35
+ // No document in window (unlike browsers)
36
+ typeof window.document === 'undefined' &&
37
+ // Has navigator (unlike Node.js)
38
+ typeof navigator !== 'undefined'
39
+ )
40
+ )
41
+
42
+ return isReactNative ? `react-native-${version}` : `react-${version}`
43
+ }, [])
44
+
45
+ const institutionsAPI = useMemo(
46
+ () => new InstitutionsAPI(connectorId, agent),
47
+ [connectorId, agent]
48
+ )
49
+ const [session] = useSession()
50
+
51
+ const [searchTermInput, setSearchTermInput] = useState('')
52
+ const [searchTerm] = useDebounce(searchTermInput, 350)
53
+ const [searchResults, setSearchResults] = useState<InstitutionsData>([])
54
+
55
+ const [isSearching, setIsSearching] = useState(false)
56
+
57
+ /**
58
+ * Start Search
59
+ * This function is used to initiate a search for institutions based on the provided term with
60
+ * a minimum length of 2 characters. Debouncing is applied to avoid excessive API calls.
61
+ */
62
+ const startSearch = useCallback((term: string) => {
63
+ if (term.trim().length < 2) {
64
+ setSearchResults([])
65
+ setIsSearching(false)
66
+ return
67
+ }
68
+
69
+ setIsSearching(true)
70
+ setSearchTermInput(term)
71
+ }, [])
72
+
73
+ const handleError = useCallback(
74
+ (message: string) => {
75
+ const errorMessage = message || 'Unknown error occurred while searching institutions'
76
+
77
+ console.error('Quiltt Institutions Search Error:', errorMessage)
78
+ if (onErrorCallback) onErrorCallback(errorMessage)
79
+ },
80
+ [onErrorCallback]
81
+ )
82
+
83
+ /**
84
+ * Run Search
85
+ * This effect will run when the searchTerm changes and is at least 2 characters long.
86
+ */
87
+ useEffect(() => {
88
+ if (!session?.token || !connectorId || !searchTerm || searchTerm.trim().length < 2) {
89
+ return
90
+ }
91
+
92
+ const abortController = new AbortController()
93
+
94
+ institutionsAPI
95
+ .search(session?.token, connectorId, searchTerm, abortController.signal)
96
+ .then((response) => {
97
+ if (!abortController.signal.aborted) {
98
+ if (response.status === 200) {
99
+ setSearchResults(response.data as InstitutionsData)
100
+ } else {
101
+ handleError((response.data as ErrorData).message || 'Failed to fetch institutions')
102
+ }
103
+ setIsSearching(false)
104
+ }
105
+ })
106
+ .catch((error) => {
107
+ if (!abortController.signal.aborted) {
108
+ handleError(error.message)
109
+ setIsSearching(false)
110
+ }
111
+ })
112
+
113
+ return () => abortController.abort()
114
+ }, [session?.token, connectorId, searchTerm, institutionsAPI, handleError])
115
+
116
+ return {
117
+ searchTerm,
118
+ searchResults,
119
+ isSearching,
120
+ setSearchTerm: startSearch,
121
+ }
122
+ }
123
+
124
+ export default useQuilttInstitutions