@quiltt/react 4.3.0 → 4.3.2

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,24 @@
1
1
  # @quiltt/react
2
2
 
3
+ ## 4.3.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#372](https://github.com/quiltt/quiltt-js/pull/372) [`c022ebe`](https://github.com/quiltt/quiltt-js/commit/c022ebed0c82404a1bdbf5abbaf6e60b49f2d07a) Thanks [@sirwolfgang](https://github.com/sirwolfgang)! - Hardened React SDK against unstable prop references by implementing ref-based callback wrappers and deep equality checks, eliminating unnecessary re-renders, event handler churn, and API calls without requiring customers to use useCallback.
8
+
9
+ - Updated dependencies [[`c022ebe`](https://github.com/quiltt/quiltt-js/commit/c022ebed0c82404a1bdbf5abbaf6e60b49f2d07a)]:
10
+ - @quiltt/core@4.3.2
11
+
12
+ ## 4.3.1
13
+
14
+ ### Patch Changes
15
+
16
+ - [#366](https://github.com/quiltt/quiltt-js/pull/366) [`dc376b5`](https://github.com/quiltt/quiltt-js/commit/dc376b52dd824d7867ca74677bbfd5c54eff5cdc) Thanks [@sirwolfgang](https://github.com/sirwolfgang)! - Warn if useQuilttConnector is unmounted while in use
17
+ - [#365](https://github.com/quiltt/quiltt-js/pull/365) [`5f6b8af`](https://github.com/quiltt/quiltt-js/commit/5f6b8af153086c77bb2227d43ae7023fb0c47985) Thanks [@rubendinho](https://github.com/rubendinho)! - Standardize SDK agent tracking header to `Quiltt-SDK-Agent` and add support for custom Apollo Links injection
18
+
19
+ - Updated dependencies [[`dc376b5`](https://github.com/quiltt/quiltt-js/commit/dc376b52dd824d7867ca74677bbfd5c54eff5cdc)]:
20
+ - @quiltt/core@4.3.1
21
+
3
22
  ## 4.3.0
4
23
 
5
24
  ### Minor Changes
@@ -7,7 +7,7 @@ import './useSession-12s-7GOn4sUn.js';
7
7
  import 'use-debounce';
8
8
  import { jsx } from 'react/jsx-runtime';
9
9
  import { ApolloProvider } from '@apollo/client/react/context/ApolloProvider.js';
10
- import { u as useQuilttSession } from './useQuilttSession-12s-QlPCR8A-.js';
10
+ import { u as useQuilttSession } from './useQuilttSession-12s-CwVw-aOM.js';
11
11
 
12
12
  const useAuthenticateSession = (auth, setSession)=>{
13
13
  const authenticateSession = useCallback(async (payload, callbacks)=>{
@@ -133,8 +133,9 @@ const useRevokeSession = (auth, session, setSession)=>{
133
133
  }
134
134
  if (obj1 instanceof Set && obj2 instanceof Set) {
135
135
  if (obj1.size !== obj2.size) return false;
136
+ const arr2 = Array.from(obj2);
136
137
  for (const item of obj1){
137
- if (!Array.from(obj2).some((value)=>isDeepEqual(item, value))) return false;
138
+ if (!arr2.some((value)=>isDeepEqual(item, value))) return false;
138
139
  }
139
140
  return true;
140
141
  }
@@ -158,16 +159,25 @@ const useRevokeSession = (auth, session, setSession)=>{
158
159
  * it into trusted storage. While this process is happening, the component is put
159
160
  * into a loading state and the children are not rendered to prevent race conditions
160
161
  * from triggering within the transitionary state.
161
- */ const QuilttAuthProvider = ({ token, children })=>{
162
+ */ const QuilttAuthProvider = ({ graphqlClient, token, children })=>{
162
163
  const { session, importSession } = useQuilttSession();
163
164
  const previousSessionRef = useRef(session);
164
- // @todo: extract into a provider so it can accessed by child components
165
- const graphQLClient = useMemo(()=>new QuilttClient({
165
+ const previousTokenRef = useRef();
166
+ // Memoize the client to avoid unnecessary re-renders
167
+ const apolloClient = useMemo(()=>graphqlClient || new QuilttClient({
166
168
  cache: new InMemoryCache()
167
- }), []);
168
- // Import passed in token
169
+ }), [
170
+ graphqlClient
171
+ ]);
172
+ // Import passed in token (only if value has changed)
169
173
  useEffect(()=>{
170
- if (token) importSession(token);
174
+ if (token && token !== previousTokenRef.current) {
175
+ importSession(token);
176
+ previousTokenRef.current = token;
177
+ } else if (!token) {
178
+ // Reset ref when token becomes undefined to allow re-import of same token later
179
+ previousTokenRef.current = undefined;
180
+ }
171
181
  }, [
172
182
  token,
173
183
  importSession
@@ -175,15 +185,15 @@ const useRevokeSession = (auth, session, setSession)=>{
175
185
  // Reset Client Store when session changes (using deep comparison)
176
186
  useEffect(()=>{
177
187
  if (!isDeepEqual(session, previousSessionRef.current)) {
178
- graphQLClient.resetStore();
188
+ apolloClient.resetStore();
179
189
  previousSessionRef.current = session;
180
190
  }
181
191
  }, [
182
192
  session,
183
- graphQLClient
193
+ apolloClient
184
194
  ]);
185
195
  return /*#__PURE__*/ jsx(ApolloProvider, {
186
- client: graphQLClient,
196
+ client: apolloClient,
187
197
  children: children
188
198
  });
189
199
  };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ConnectorSDKCallbacks, Maybe, QuilttJWT, PasscodePayload, UnprocessableData, AuthAPI, UsernamePayload, ConnectorSDKConnectorOptions, InstitutionsData } from '@quiltt/core';
1
+ import { ConnectorSDKCallbacks, Maybe, QuilttJWT, PasscodePayload, UnprocessableData, AuthAPI, UsernamePayload, ConnectorSDKConnectorOptions, InstitutionsData, QuilttClient } from '@quiltt/core';
2
2
  export * from '@quiltt/core';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import { JSX, ComponentType, ElementType, PropsWithChildren, MouseEvent, RefObject, useEffect, Dispatch, SetStateAction, FC } from 'react';
@@ -159,7 +159,9 @@ declare const useQuilttSettings: () => {
159
159
  declare const useStorage: <T>(key: string, initialState?: Maybe<T>) => [Maybe<T> | undefined, Dispatch<SetStateAction<Maybe<T> | undefined>>];
160
160
 
161
161
  type QuilttAuthProviderProps = PropsWithChildren & {
162
- /** The Session token obtained from the server */
162
+ /** A custom QuilttClient instance to use instead of the default */
163
+ graphqlClient?: QuilttClient;
164
+ /** The Quiltt Session token obtained from the server */
163
165
  token?: string;
164
166
  };
165
167
  /**
@@ -170,19 +172,14 @@ type QuilttAuthProviderProps = PropsWithChildren & {
170
172
  */
171
173
  declare const QuilttAuthProvider: FC<QuilttAuthProviderProps>;
172
174
 
173
- type QuilttProviderProps = PropsWithChildren & {
174
- /** The client ID for the client-side Auth API */
175
- clientId?: string;
176
- /** The Session token obtained from the server */
177
- token?: string;
178
- };
179
- declare const QuilttProvider: FC<QuilttProviderProps>;
180
-
181
175
  type QuilttSettingsProviderProps = PropsWithChildren & {
182
- /** The Client ID to use for the client-side Auth API */
176
+ /** The Client ID to use for the passwordless Auth API */
183
177
  clientId?: string;
184
178
  };
185
179
  declare const QuilttSettingsProvider: FC<QuilttSettingsProviderProps>;
186
180
 
181
+ type QuilttProviderProps = QuilttSettingsProviderProps & QuilttAuthProviderProps;
182
+ declare const QuilttProvider: FC<QuilttProviderProps>;
183
+
187
184
  export { QuilttAuthProvider, QuilttButton, QuilttContainer, QuilttProvider, QuilttSettingsProvider, useAuthenticateSession, useEventListener, useIdentifySession, useImportSession, useIsomorphicLayoutEffect, useQuilttClient, useQuilttConnector, useQuilttInstitutions, useQuilttSession, useQuilttSettings, useRevokeSession, useSession, useStorage };
188
- export type { AuthenticateSession, IdentifySession, ImportSession, RevokeSession, SetSession, UseQuilttInstitutions, UseQuilttSession };
185
+ export type { AuthenticateSession, IdentifySession, ImportSession, QuilttAuthProviderProps, QuilttSettingsProviderProps, RevokeSession, SetSession, UseQuilttInstitutions, UseQuilttSession };
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  export * from '@quiltt/core';
2
2
  import { jsx } from 'react/jsx-runtime';
3
3
  import { useRef, useEffect } from 'react';
4
- import { u as useQuilttConnector } from './useQuilttConnector-12s-BaTUXD56.js';
5
- import { i as isDeepEqual, Q as QuilttAuthProvider } from './QuilttAuthProvider-12s-DNnAmo8E.js';
6
- export { b as useAuthenticateSession, a as useIdentifySession, u as useImportSession, c as useRevokeSession } from './QuilttAuthProvider-12s-DNnAmo8E.js';
4
+ import { u as useQuilttConnector } from './useQuilttConnector-12s-BJljrsWT.js';
5
+ import { i as isDeepEqual, Q as QuilttAuthProvider } from './QuilttAuthProvider-12s-D-Wr7LUT.js';
6
+ export { b as useAuthenticateSession, a as useIdentifySession, u as useImportSession, c as useRevokeSession } from './QuilttAuthProvider-12s-D-Wr7LUT.js';
7
7
  export { u as useEventListener } from './useEventListener-12s-D_-6QIXa.js';
8
8
  export { u as useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect-12s-DeTHOKz1.js';
9
9
  export { u as useQuilttClient } from './useQuilttClient-12s-CAAUait1.js';
10
- export { u as useQuilttInstitutions } from './useQuilttInstitutions-12s-Djd5k9Ae.js';
11
- export { u as useQuilttSession } from './useQuilttSession-12s-QlPCR8A-.js';
10
+ export { u as useQuilttInstitutions } from './useQuilttInstitutions-12s-PJd2C3BF.js';
11
+ export { u as useQuilttSession } from './useQuilttSession-12s-CwVw-aOM.js';
12
12
  export { u as useQuilttSettings } from './useQuilttSettings-12s--rCJoNHD.js';
13
13
  export { u as useSession } from './useSession-12s-7GOn4sUn.js';
14
14
  export { u as useStorage } from './useStorage-12s-DHcq3Kuh.js';
@@ -78,9 +78,11 @@ import { Q as QuilttSettingsProvider } from './QuilttSettingsProvider-12s-ZcmFmO
78
78
  // 1. Pre-open validation
79
79
  // 2. Preventing opening via event.preventDefault()
80
80
  // 3. Setting up state before connector opens
81
- if (onClick) onClick(event);
82
- // Then open the Quiltt connector
83
- open();
81
+ onClick?.(event);
82
+ // Only open if event wasn't prevented
83
+ if (!event.defaultPrevented) {
84
+ open();
85
+ }
84
86
  };
85
87
  // Generate key for forced remounting if enabled, but respect user-provided key
86
88
  const buttonKey = props.key ?? (forceRemountOnConnectionChange ? `${connectorId}-${connectionId || 'no-connection'}` : undefined);
@@ -159,11 +161,12 @@ import { Q as QuilttSettingsProvider } from './QuilttSettingsProvider-12s-ZcmFmO
159
161
  }, containerKey);
160
162
  };
161
163
 
162
- const QuilttProvider = ({ clientId, token, children })=>{
164
+ const QuilttProvider = ({ clientId, graphqlClient, token, children })=>{
163
165
  return /*#__PURE__*/ jsx(QuilttSettingsProvider, {
164
166
  clientId: clientId,
165
167
  children: /*#__PURE__*/ jsx(QuilttAuthProvider, {
166
168
  token: token,
169
+ graphqlClient: graphqlClient,
167
170
  children: children
168
171
  })
169
172
  });
@@ -0,0 +1,173 @@
1
+ 'use client';
2
+ import { useState, useRef, useEffect, useCallback } from 'react';
3
+ import { cdnBase } from '@quiltt/core';
4
+ import { u as useQuilttSession } from './useQuilttSession-12s-CwVw-aOM.js';
5
+ import { u as useScript } from './useScript-12s-JCgaTW9n.js';
6
+ import { i as isDeepEqual } from './QuilttAuthProvider-12s-D-Wr7LUT.js';
7
+
8
+ var version = "4.3.2";
9
+
10
+ const useQuilttConnector = (connectorId, options)=>{
11
+ const status = useScript(`${cdnBase}/v1/connector.js?agent=react-${version}`, {
12
+ nonce: options?.nonce
13
+ });
14
+ // This ensures we're not destructuring `session` before the script has loaded
15
+ const useQuilttSessionReturn = useQuilttSession();
16
+ const session = useQuilttSessionReturn?.session || null;
17
+ const [connector, setConnector] = useState();
18
+ const [isOpening, setIsOpening] = useState(false);
19
+ // Keep track of the previous connectionId to detect changes
20
+ const prevConnectionIdRef = useRef(options?.connectionId);
21
+ const prevConnectorIdRef = useRef(connectorId);
22
+ const prevInstitutionRef = useRef(options?.institution);
23
+ const connectorCreatedRef = useRef(false);
24
+ // Track whether the connector is currently open
25
+ const isConnectorOpenRef = useRef(false);
26
+ // Store callbacks in refs to maintain stable references
27
+ const callbacksRef = useRef(options || {});
28
+ useEffect(()=>{
29
+ callbacksRef.current = options || {};
30
+ });
31
+ // Set Session
32
+ // biome-ignore lint/correctness/useExhaustiveDependencies: trigger effects when script status changes too
33
+ useEffect(()=>{
34
+ if (typeof Quiltt === 'undefined') return;
35
+ Quiltt.authenticate(session?.token);
36
+ }, [
37
+ status,
38
+ session?.token
39
+ ]);
40
+ // Set Connector
41
+ // biome-ignore lint/correctness/useExhaustiveDependencies: trigger effects when script status changes too
42
+ useEffect(()=>{
43
+ if (typeof Quiltt === 'undefined' || !connectorId) return;
44
+ const currentConnectionId = options?.connectionId;
45
+ const currentInstitution = options?.institution;
46
+ // Check for changes - use deep equality for institution object
47
+ const connectionIdChanged = prevConnectionIdRef.current !== currentConnectionId;
48
+ const connectorIdChanged = prevConnectorIdRef.current !== connectorId;
49
+ const institutionChanged = !isDeepEqual(prevInstitutionRef.current, currentInstitution);
50
+ const hasChanges = connectionIdChanged || connectorIdChanged || institutionChanged || !connectorCreatedRef.current;
51
+ // Update if there are changes, regardless of what the changes are
52
+ if (hasChanges) {
53
+ if (currentConnectionId) {
54
+ // Always use reconnect when connectionId is available
55
+ setConnector(Quiltt.reconnect(connectorId, {
56
+ connectionId: currentConnectionId
57
+ }));
58
+ } else {
59
+ // Use connect for new connections without connectionId
60
+ setConnector(Quiltt.connect(connectorId, {
61
+ institution: currentInstitution
62
+ }));
63
+ }
64
+ // Update refs
65
+ connectorCreatedRef.current = true;
66
+ prevConnectionIdRef.current = currentConnectionId;
67
+ prevConnectorIdRef.current = connectorId;
68
+ prevInstitutionRef.current = currentInstitution;
69
+ }
70
+ }, [
71
+ connectorId,
72
+ options?.connectionId,
73
+ options?.institution,
74
+ status
75
+ ]);
76
+ // Internal handlers to track connector state (stable references)
77
+ const handleOpen = useCallback((metadata)=>{
78
+ isConnectorOpenRef.current = true;
79
+ callbacksRef.current?.onOpen?.(metadata);
80
+ }, []);
81
+ const handleExit = useCallback((type, metadata)=>{
82
+ isConnectorOpenRef.current = false;
83
+ callbacksRef.current?.onExit?.(type, metadata);
84
+ }, []);
85
+ // Create stable wrapper functions for callbacks
86
+ const stableOnEvent = useCallback((type, metadata)=>{
87
+ callbacksRef.current?.onEvent?.(type, metadata);
88
+ }, []);
89
+ const stableOnLoad = useCallback((metadata)=>{
90
+ callbacksRef.current?.onLoad?.(metadata);
91
+ }, []);
92
+ const stableOnExitSuccess = useCallback((metadata)=>{
93
+ callbacksRef.current?.onExitSuccess?.(metadata);
94
+ }, []);
95
+ const stableOnExitAbort = useCallback((metadata)=>{
96
+ callbacksRef.current?.onExitAbort?.(metadata);
97
+ }, []);
98
+ const stableOnExitError = useCallback((metadata)=>{
99
+ callbacksRef.current?.onExitError?.(metadata);
100
+ }, []);
101
+ // Register event handlers (only re-runs when connector changes)
102
+ useEffect(()=>{
103
+ if (!connector) return;
104
+ // Capture which handlers we're registering to ensure proper cleanup
105
+ const registered = {
106
+ onEvent: callbacksRef.current?.onEvent ? stableOnEvent : null,
107
+ onOpen: callbacksRef.current?.onOpen ? handleOpen : null,
108
+ onLoad: callbacksRef.current?.onLoad ? stableOnLoad : null,
109
+ onExit: callbacksRef.current?.onExit ? handleExit : null,
110
+ onExitSuccess: callbacksRef.current?.onExitSuccess ? stableOnExitSuccess : null,
111
+ onExitAbort: callbacksRef.current?.onExitAbort ? stableOnExitAbort : null,
112
+ onExitError: callbacksRef.current?.onExitError ? stableOnExitError : null
113
+ };
114
+ if (registered.onEvent) connector.onEvent(registered.onEvent);
115
+ if (registered.onOpen) connector.onOpen(registered.onOpen);
116
+ if (registered.onLoad) connector.onLoad(registered.onLoad);
117
+ if (registered.onExit) connector.onExit(registered.onExit);
118
+ if (registered.onExitSuccess) connector.onExitSuccess(registered.onExitSuccess);
119
+ if (registered.onExitAbort) connector.onExitAbort(registered.onExitAbort);
120
+ if (registered.onExitError) connector.onExitError(registered.onExitError);
121
+ return ()=>{
122
+ if (registered.onEvent) connector.offEvent(registered.onEvent);
123
+ if (registered.onOpen) connector.offOpen(registered.onOpen);
124
+ if (registered.onLoad) connector.offLoad(registered.onLoad);
125
+ if (registered.onExit) connector.offExit(registered.onExit);
126
+ if (registered.onExitSuccess) connector.offExitSuccess(registered.onExitSuccess);
127
+ if (registered.onExitAbort) connector.offExitAbort(registered.onExitAbort);
128
+ if (registered.onExitError) connector.offExitError(registered.onExitError);
129
+ };
130
+ }, [
131
+ connector,
132
+ stableOnEvent,
133
+ handleOpen,
134
+ stableOnLoad,
135
+ handleExit,
136
+ stableOnExitSuccess,
137
+ stableOnExitAbort,
138
+ stableOnExitError
139
+ ]);
140
+ // This is used to hide any potential race conditions from usage; allowing
141
+ // interaction before the script may have loaded.
142
+ useEffect(()=>{
143
+ if (connector && isOpening) {
144
+ setIsOpening(false);
145
+ connector.open();
146
+ }
147
+ }, [
148
+ connector,
149
+ isOpening
150
+ ]);
151
+ // Cleanup effect - runs when the hook is torn down
152
+ useEffect(()=>{
153
+ return ()=>{
154
+ if (isConnectorOpenRef.current) {
155
+ console.error('[Quiltt] useQuilttConnector: Component unmounted while Connector is still open. ' + 'This may lead to memory leaks or unexpected behavior. ' + 'Ensure the Connector is properly closed before component unmount.');
156
+ }
157
+ };
158
+ }, []);
159
+ const open = useCallback(()=>{
160
+ if (connectorId) {
161
+ setIsOpening(true);
162
+ } else {
163
+ throw new Error('Must provide `connectorId` to `open` Quiltt Connector with Method Call');
164
+ }
165
+ }, [
166
+ connectorId
167
+ ]);
168
+ return {
169
+ open
170
+ };
171
+ };
172
+
173
+ export { useQuilttConnector as u, version as v };
@@ -1,8 +1,8 @@
1
1
  'use client';
2
- import { useMemo, useState, useCallback, useEffect } from 'react';
2
+ import { useMemo, useState, useRef, useEffect, useCallback } from 'react';
3
3
  import { InstitutionsAPI } from '@quiltt/core';
4
4
  import { useDebounce } from 'use-debounce';
5
- import { v as version } from './useQuilttConnector-12s-BaTUXD56.js';
5
+ import { v as version } from './useQuilttConnector-12s-BJljrsWT.js';
6
6
  import { u as useSession } from './useSession-12s-7GOn4sUn.js';
7
7
 
8
8
  const useQuilttInstitutions = (connectorId, onErrorCallback)=>{
@@ -27,6 +27,11 @@ const useQuilttInstitutions = (connectorId, onErrorCallback)=>{
27
27
  const [searchTerm] = useDebounce(searchTermInput, 350);
28
28
  const [searchResults, setSearchResults] = useState([]);
29
29
  const [isSearching, setIsSearching] = useState(false);
30
+ // Store callback in ref to maintain stable reference
31
+ const onErrorCallbackRef = useRef(onErrorCallback);
32
+ useEffect(()=>{
33
+ onErrorCallbackRef.current = onErrorCallback;
34
+ });
30
35
  /**
31
36
  * Start Search
32
37
  * This function is used to initiate a search for institutions based on the provided term with
@@ -43,10 +48,8 @@ const useQuilttInstitutions = (connectorId, onErrorCallback)=>{
43
48
  const handleError = useCallback((message)=>{
44
49
  const errorMessage = message || 'Unknown error occurred while searching institutions';
45
50
  console.error('Quiltt Institutions Search Error:', errorMessage);
46
- if (onErrorCallback) onErrorCallback(errorMessage);
47
- }, [
48
- onErrorCallback
49
- ]);
51
+ if (onErrorCallbackRef.current) onErrorCallbackRef.current(errorMessage);
52
+ }, []);
50
53
  /**
51
54
  * Run Search
52
55
  * This effect will run when the searchTerm changes and is at least 2 characters long.
@@ -66,7 +69,7 @@ const useQuilttInstitutions = (connectorId, onErrorCallback)=>{
66
69
  }
67
70
  }).catch((error)=>{
68
71
  if (!abortController.signal.aborted) {
69
- handleError(error.message);
72
+ handleError(error?.message);
70
73
  setIsSearching(false);
71
74
  }
72
75
  });
@@ -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-12s-DNnAmo8E.js';
4
+ import { u as useImportSession, a as useIdentifySession, b as useAuthenticateSession, c as useRevokeSession } from './QuilttAuthProvider-12s-D-Wr7LUT.js';
5
5
  import { u as useQuilttSettings } from './useQuilttSettings-12s--rCJoNHD.js';
6
6
  import { u as useSession } from './useSession-12s-7GOn4sUn.js';
7
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quiltt/react",
3
- "version": "4.3.0",
3
+ "version": "4.3.2",
4
4
  "description": "React Components and Hooks for Quiltt Connector",
5
5
  "keywords": [
6
6
  "quiltt",
@@ -36,7 +36,7 @@
36
36
  "dependencies": {
37
37
  "@apollo/client": "^3.14.0",
38
38
  "use-debounce": "^10.0.4",
39
- "@quiltt/core": "4.3.0"
39
+ "@quiltt/core": "4.3.2"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@biomejs/biome": "2.2.4",
@@ -127,10 +127,12 @@ export const QuilttButton = <T extends ElementType = 'button'>({
127
127
  // 1. Pre-open validation
128
128
  // 2. Preventing opening via event.preventDefault()
129
129
  // 3. Setting up state before connector opens
130
- if (onClick) onClick(event)
130
+ onClick?.(event)
131
131
 
132
- // Then open the Quiltt connector
133
- open()
132
+ // Only open if event wasn't prevented
133
+ if (!event.defaultPrevented) {
134
+ open()
135
+ }
134
136
  }
135
137
 
136
138
  // Generate key for forced remounting if enabled, but respect user-provided key
@@ -11,6 +11,7 @@ import { cdnBase } from '@quiltt/core'
11
11
 
12
12
  import { useQuilttSession } from '@/hooks/useQuilttSession'
13
13
  import { useScript } from '@/hooks/useScript'
14
+ import { isDeepEqual } from '@/utils/isDeepEqual'
14
15
  import { version } from '@/version'
15
16
 
16
17
  declare const Quiltt: ConnectorSDK
@@ -33,8 +34,18 @@ export const useQuilttConnector = (
33
34
  // Keep track of the previous connectionId to detect changes
34
35
  const prevConnectionIdRef = useRef<string | undefined>(options?.connectionId)
35
36
  const prevConnectorIdRef = useRef<string | undefined>(connectorId)
37
+ const prevInstitutionRef = useRef<string | undefined>(options?.institution)
36
38
  const connectorCreatedRef = useRef<boolean>(false)
37
39
 
40
+ // Track whether the connector is currently open
41
+ const isConnectorOpenRef = useRef<boolean>(false)
42
+
43
+ // Store callbacks in refs to maintain stable references
44
+ const callbacksRef = useRef<ConnectorSDKConnectorOptions>(options || {})
45
+ useEffect(() => {
46
+ callbacksRef.current = options || {}
47
+ })
48
+
38
49
  // Set Session
39
50
  // biome-ignore lint/correctness/useExhaustiveDependencies: trigger effects when script status changes too
40
51
  useEffect(() => {
@@ -51,10 +62,15 @@ export const useQuilttConnector = (
51
62
  const currentConnectionId = options?.connectionId
52
63
  const currentInstitution = options?.institution
53
64
 
54
- // Check for changes
65
+ // Check for changes - use deep equality for institution object
55
66
  const connectionIdChanged = prevConnectionIdRef.current !== currentConnectionId
56
67
  const connectorIdChanged = prevConnectorIdRef.current !== connectorId
57
- const hasChanges = connectionIdChanged || connectorIdChanged || !connectorCreatedRef.current
68
+ const institutionChanged = !isDeepEqual(prevInstitutionRef.current, currentInstitution)
69
+ const hasChanges =
70
+ connectionIdChanged ||
71
+ connectorIdChanged ||
72
+ institutionChanged ||
73
+ !connectorCreatedRef.current
58
74
 
59
75
  // Update if there are changes, regardless of what the changes are
60
76
  if (hasChanges) {
@@ -70,49 +86,83 @@ export const useQuilttConnector = (
70
86
  connectorCreatedRef.current = true
71
87
  prevConnectionIdRef.current = currentConnectionId
72
88
  prevConnectorIdRef.current = connectorId
89
+ prevInstitutionRef.current = currentInstitution
73
90
  }
74
91
  }, [connectorId, options?.connectionId, options?.institution, status])
75
92
 
76
- // Register event handlers
93
+ // Internal handlers to track connector state (stable references)
94
+ const handleOpen = useCallback((metadata: any) => {
95
+ isConnectorOpenRef.current = true
96
+ callbacksRef.current?.onOpen?.(metadata)
97
+ }, [])
98
+
99
+ const handleExit = useCallback((type: any, metadata: any) => {
100
+ isConnectorOpenRef.current = false
101
+ callbacksRef.current?.onExit?.(type, metadata)
102
+ }, [])
103
+
104
+ // Create stable wrapper functions for callbacks
105
+ const stableOnEvent = useCallback((type: any, metadata: any) => {
106
+ callbacksRef.current?.onEvent?.(type, metadata)
107
+ }, [])
108
+
109
+ const stableOnLoad = useCallback((metadata: any) => {
110
+ callbacksRef.current?.onLoad?.(metadata)
111
+ }, [])
112
+
113
+ const stableOnExitSuccess = useCallback((metadata: any) => {
114
+ callbacksRef.current?.onExitSuccess?.(metadata)
115
+ }, [])
116
+
117
+ const stableOnExitAbort = useCallback((metadata: any) => {
118
+ callbacksRef.current?.onExitAbort?.(metadata)
119
+ }, [])
120
+
121
+ const stableOnExitError = useCallback((metadata: any) => {
122
+ callbacksRef.current?.onExitError?.(metadata)
123
+ }, [])
124
+
125
+ // Register event handlers (only re-runs when connector changes)
77
126
  useEffect(() => {
78
127
  if (!connector) return
79
128
 
80
- const handlers = {
81
- onEvent: options?.onEvent,
82
- onOpen: options?.onOpen,
83
- onLoad: options?.onLoad,
84
- onExit: options?.onExit,
85
- onExitSuccess: options?.onExitSuccess,
86
- onExitAbort: options?.onExitAbort,
87
- onExitError: options?.onExitError,
129
+ // Capture which handlers we're registering to ensure proper cleanup
130
+ const registered = {
131
+ onEvent: callbacksRef.current?.onEvent ? stableOnEvent : null,
132
+ onOpen: callbacksRef.current?.onOpen ? handleOpen : null,
133
+ onLoad: callbacksRef.current?.onLoad ? stableOnLoad : null,
134
+ onExit: callbacksRef.current?.onExit ? handleExit : null,
135
+ onExitSuccess: callbacksRef.current?.onExitSuccess ? stableOnExitSuccess : null,
136
+ onExitAbort: callbacksRef.current?.onExitAbort ? stableOnExitAbort : null,
137
+ onExitError: callbacksRef.current?.onExitError ? stableOnExitError : null,
88
138
  }
89
139
 
90
- if (handlers.onEvent) connector.onEvent(handlers.onEvent)
91
- if (handlers.onOpen) connector.onOpen(handlers.onOpen)
92
- if (handlers.onLoad) connector.onLoad(handlers.onLoad)
93
- if (handlers.onExit) connector.onExit(handlers.onExit)
94
- if (handlers.onExitSuccess) connector.onExitSuccess(handlers.onExitSuccess)
95
- if (handlers.onExitAbort) connector.onExitAbort(handlers.onExitAbort)
96
- if (handlers.onExitError) connector.onExitError(handlers.onExitError)
140
+ if (registered.onEvent) connector.onEvent(registered.onEvent)
141
+ if (registered.onOpen) connector.onOpen(registered.onOpen)
142
+ if (registered.onLoad) connector.onLoad(registered.onLoad)
143
+ if (registered.onExit) connector.onExit(registered.onExit)
144
+ if (registered.onExitSuccess) connector.onExitSuccess(registered.onExitSuccess)
145
+ if (registered.onExitAbort) connector.onExitAbort(registered.onExitAbort)
146
+ if (registered.onExitError) connector.onExitError(registered.onExitError)
97
147
 
98
148
  return () => {
99
- if (handlers.onEvent) connector.offEvent(handlers.onEvent)
100
- if (handlers.onOpen) connector.offOpen(handlers.onOpen)
101
- if (handlers.onLoad) connector.offLoad(handlers.onLoad)
102
- if (handlers.onExit) connector.offExit(handlers.onExit)
103
- if (handlers.onExitSuccess) connector.offExitSuccess(handlers.onExitSuccess)
104
- if (handlers.onExitAbort) connector.offExitAbort(handlers.onExitAbort)
105
- if (handlers.onExitError) connector.offExitError(handlers.onExitError)
149
+ if (registered.onEvent) connector.offEvent(registered.onEvent)
150
+ if (registered.onOpen) connector.offOpen(registered.onOpen)
151
+ if (registered.onLoad) connector.offLoad(registered.onLoad)
152
+ if (registered.onExit) connector.offExit(registered.onExit)
153
+ if (registered.onExitSuccess) connector.offExitSuccess(registered.onExitSuccess)
154
+ if (registered.onExitAbort) connector.offExitAbort(registered.onExitAbort)
155
+ if (registered.onExitError) connector.offExitError(registered.onExitError)
106
156
  }
107
157
  }, [
108
158
  connector,
109
- options?.onEvent,
110
- options?.onOpen,
111
- options?.onLoad,
112
- options?.onExit,
113
- options?.onExitSuccess,
114
- options?.onExitAbort,
115
- options?.onExitError,
159
+ stableOnEvent,
160
+ handleOpen,
161
+ stableOnLoad,
162
+ handleExit,
163
+ stableOnExitSuccess,
164
+ stableOnExitAbort,
165
+ stableOnExitError,
116
166
  ])
117
167
 
118
168
  // This is used to hide any potential race conditions from usage; allowing
@@ -124,6 +174,19 @@ export const useQuilttConnector = (
124
174
  }
125
175
  }, [connector, isOpening])
126
176
 
177
+ // Cleanup effect - runs when the hook is torn down
178
+ useEffect(() => {
179
+ return () => {
180
+ if (isConnectorOpenRef.current) {
181
+ console.error(
182
+ '[Quiltt] useQuilttConnector: Component unmounted while Connector is still open. ' +
183
+ 'This may lead to memory leaks or unexpected behavior. ' +
184
+ 'Ensure the Connector is properly closed before component unmount.'
185
+ )
186
+ }
187
+ }
188
+ }, [])
189
+
127
190
  const open = useCallback(() => {
128
191
  if (connectorId) {
129
192
  setIsOpening(true)
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useCallback, useEffect, useMemo, useState } from 'react'
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
4
4
 
5
5
  import type { ErrorData, InstitutionsData } from '@quiltt/core'
6
6
  import { InstitutionsAPI } from '@quiltt/core'
@@ -53,6 +53,12 @@ export const useQuilttInstitutions: UseQuilttInstitutions = (connectorId, onErro
53
53
 
54
54
  const [isSearching, setIsSearching] = useState(false)
55
55
 
56
+ // Store callback in ref to maintain stable reference
57
+ const onErrorCallbackRef = useRef(onErrorCallback)
58
+ useEffect(() => {
59
+ onErrorCallbackRef.current = onErrorCallback
60
+ })
61
+
56
62
  /**
57
63
  * Start Search
58
64
  * This function is used to initiate a search for institutions based on the provided term with
@@ -69,15 +75,12 @@ export const useQuilttInstitutions: UseQuilttInstitutions = (connectorId, onErro
69
75
  setSearchTermInput(term)
70
76
  }, [])
71
77
 
72
- const handleError = useCallback(
73
- (message: string) => {
74
- const errorMessage = message || 'Unknown error occurred while searching institutions'
78
+ const handleError = useCallback((message: string) => {
79
+ const errorMessage = message || 'Unknown error occurred while searching institutions'
75
80
 
76
- console.error('Quiltt Institutions Search Error:', errorMessage)
77
- if (onErrorCallback) onErrorCallback(errorMessage)
78
- },
79
- [onErrorCallback]
80
- )
81
+ console.error('Quiltt Institutions Search Error:', errorMessage)
82
+ if (onErrorCallbackRef.current) onErrorCallbackRef.current(errorMessage)
83
+ }, [])
81
84
 
82
85
  /**
83
86
  * Run Search
@@ -104,7 +107,7 @@ export const useQuilttInstitutions: UseQuilttInstitutions = (connectorId, onErro
104
107
  })
105
108
  .catch((error) => {
106
109
  if (!abortController.signal.aborted) {
107
- handleError(error.message)
110
+ handleError(error?.message)
108
111
  setIsSearching(false)
109
112
  }
110
113
  })
@@ -9,8 +9,10 @@ import { InMemoryCache, QuilttClient } from '@quiltt/core'
9
9
  import { useQuilttSession } from '@/hooks'
10
10
  import { isDeepEqual } from '@/utils'
11
11
 
12
- type QuilttAuthProviderProps = PropsWithChildren & {
13
- /** The Session token obtained from the server */
12
+ export type QuilttAuthProviderProps = PropsWithChildren & {
13
+ /** A custom QuilttClient instance to use instead of the default */
14
+ graphqlClient?: QuilttClient
15
+ /** The Quiltt Session token obtained from the server */
14
16
  token?: string
15
17
  }
16
18
 
@@ -20,33 +22,45 @@ type QuilttAuthProviderProps = PropsWithChildren & {
20
22
  * into a loading state and the children are not rendered to prevent race conditions
21
23
  * from triggering within the transitionary state.
22
24
  */
23
- export const QuilttAuthProvider: FC<QuilttAuthProviderProps> = ({ token, children }) => {
25
+ export const QuilttAuthProvider: FC<QuilttAuthProviderProps> = ({
26
+ graphqlClient,
27
+ token,
28
+ children,
29
+ }) => {
24
30
  const { session, importSession } = useQuilttSession()
25
31
  const previousSessionRef = useRef(session)
32
+ const previousTokenRef = useRef<string | undefined>()
26
33
 
27
- // @todo: extract into a provider so it can accessed by child components
28
- const graphQLClient = useMemo(
34
+ // Memoize the client to avoid unnecessary re-renders
35
+ const apolloClient = useMemo(
29
36
  () =>
37
+ graphqlClient ||
30
38
  new QuilttClient({
31
39
  cache: new InMemoryCache(),
32
40
  }),
33
- []
41
+ [graphqlClient]
34
42
  )
35
43
 
36
- // Import passed in token
44
+ // Import passed in token (only if value has changed)
37
45
  useEffect(() => {
38
- if (token) importSession(token)
46
+ if (token && token !== previousTokenRef.current) {
47
+ importSession(token)
48
+ previousTokenRef.current = token
49
+ } else if (!token) {
50
+ // Reset ref when token becomes undefined to allow re-import of same token later
51
+ previousTokenRef.current = undefined
52
+ }
39
53
  }, [token, importSession])
40
54
 
41
55
  // Reset Client Store when session changes (using deep comparison)
42
56
  useEffect(() => {
43
57
  if (!isDeepEqual(session, previousSessionRef.current)) {
44
- graphQLClient.resetStore()
58
+ apolloClient.resetStore()
45
59
  previousSessionRef.current = session
46
60
  }
47
- }, [session, graphQLClient])
61
+ }, [session, apolloClient])
48
62
 
49
- return <ApolloProvider client={graphQLClient}>{children}</ApolloProvider>
63
+ return <ApolloProvider client={apolloClient}>{children}</ApolloProvider>
50
64
  }
51
65
 
52
66
  export default QuilttAuthProvider
@@ -1,19 +1,23 @@
1
- import type { FC, PropsWithChildren } from 'react'
1
+ import type { FC } from 'react'
2
2
 
3
+ import type { QuilttAuthProviderProps } from './QuilttAuthProvider'
3
4
  import { QuilttAuthProvider } from './QuilttAuthProvider'
5
+ import type { QuilttSettingsProviderProps } from './QuilttSettingsProvider'
4
6
  import { QuilttSettingsProvider } from './QuilttSettingsProvider'
5
7
 
6
- type QuilttProviderProps = PropsWithChildren & {
7
- /** The client ID for the client-side Auth API */
8
- clientId?: string
9
- /** The Session token obtained from the server */
10
- token?: string
11
- }
8
+ type QuilttProviderProps = QuilttSettingsProviderProps & QuilttAuthProviderProps
12
9
 
13
- export const QuilttProvider: FC<QuilttProviderProps> = ({ clientId, token, children }) => {
10
+ export const QuilttProvider: FC<QuilttProviderProps> = ({
11
+ clientId,
12
+ graphqlClient,
13
+ token,
14
+ children,
15
+ }) => {
14
16
  return (
15
17
  <QuilttSettingsProvider clientId={clientId}>
16
- <QuilttAuthProvider token={token}>{children}</QuilttAuthProvider>
18
+ <QuilttAuthProvider token={token} graphqlClient={graphqlClient}>
19
+ {children}
20
+ </QuilttAuthProvider>
17
21
  </QuilttSettingsProvider>
18
22
  )
19
23
  }
@@ -5,8 +5,8 @@ import { useState } from 'react'
5
5
 
6
6
  import { QuilttSettings } from '@/contexts/QuilttSettings'
7
7
 
8
- type QuilttSettingsProviderProps = PropsWithChildren & {
9
- /** The Client ID to use for the client-side Auth API */
8
+ export type QuilttSettingsProviderProps = PropsWithChildren & {
9
+ /** The Client ID to use for the passwordless Auth API */
10
10
  clientId?: string
11
11
  }
12
12
 
@@ -31,8 +31,9 @@ export const isDeepEqual = (obj1: unknown, obj2: unknown): boolean => {
31
31
  }
32
32
  if (obj1 instanceof Set && obj2 instanceof Set) {
33
33
  if (obj1.size !== obj2.size) return false
34
+ const arr2 = Array.from(obj2)
34
35
  for (const item of obj1) {
35
- if (!Array.from(obj2).some((value) => isDeepEqual(item, value))) return false
36
+ if (!arr2.some((value) => isDeepEqual(item, value))) return false
36
37
  }
37
38
  return true
38
39
  }
@@ -1,128 +0,0 @@
1
- 'use client';
2
- import { useState, useRef, useEffect, useCallback } from 'react';
3
- import { cdnBase } from '@quiltt/core';
4
- import { u as useQuilttSession } from './useQuilttSession-12s-QlPCR8A-.js';
5
- import { u as useScript } from './useScript-12s-JCgaTW9n.js';
6
-
7
- var version = "4.3.0";
8
-
9
- const useQuilttConnector = (connectorId, options)=>{
10
- const status = useScript(`${cdnBase}/v1/connector.js?agent=react-${version}`, {
11
- nonce: options?.nonce
12
- });
13
- // This ensures we're not destructuring `session` before the script has loaded
14
- const useQuilttSessionReturn = useQuilttSession();
15
- const session = useQuilttSessionReturn?.session || null;
16
- const [connector, setConnector] = useState();
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);
22
- // Set Session
23
- // biome-ignore lint/correctness/useExhaustiveDependencies: trigger effects when script status changes too
24
- useEffect(()=>{
25
- if (typeof Quiltt === 'undefined') return;
26
- Quiltt.authenticate(session?.token);
27
- }, [
28
- status,
29
- session?.token
30
- ]);
31
- // Set Connector
32
- // biome-ignore lint/correctness/useExhaustiveDependencies: trigger effects when script status changes too
33
- useEffect(()=>{
34
- if (typeof Quiltt === 'undefined' || !connectorId) return;
35
- const currentConnectionId = options?.connectionId;
36
- const currentInstitution = options?.institution;
37
- // Check for changes
38
- const connectionIdChanged = prevConnectionIdRef.current !== currentConnectionId;
39
- const connectorIdChanged = prevConnectorIdRef.current !== connectorId;
40
- const hasChanges = connectionIdChanged || connectorIdChanged || !connectorCreatedRef.current;
41
- // Update if there are changes, regardless of what the changes are
42
- if (hasChanges) {
43
- if (currentConnectionId) {
44
- // Always use reconnect when connectionId is available
45
- setConnector(Quiltt.reconnect(connectorId, {
46
- connectionId: currentConnectionId
47
- }));
48
- } else {
49
- // Use connect for new connections without connectionId
50
- setConnector(Quiltt.connect(connectorId, {
51
- institution: currentInstitution
52
- }));
53
- }
54
- // Update refs
55
- connectorCreatedRef.current = true;
56
- prevConnectionIdRef.current = currentConnectionId;
57
- prevConnectorIdRef.current = connectorId;
58
- }
59
- }, [
60
- connectorId,
61
- options?.connectionId,
62
- options?.institution,
63
- status
64
- ]);
65
- // Register event handlers
66
- useEffect(()=>{
67
- if (!connector) return;
68
- const handlers = {
69
- onEvent: options?.onEvent,
70
- onOpen: options?.onOpen,
71
- onLoad: options?.onLoad,
72
- onExit: options?.onExit,
73
- onExitSuccess: options?.onExitSuccess,
74
- onExitAbort: options?.onExitAbort,
75
- onExitError: options?.onExitError
76
- };
77
- if (handlers.onEvent) connector.onEvent(handlers.onEvent);
78
- if (handlers.onOpen) connector.onOpen(handlers.onOpen);
79
- if (handlers.onLoad) connector.onLoad(handlers.onLoad);
80
- if (handlers.onExit) connector.onExit(handlers.onExit);
81
- if (handlers.onExitSuccess) connector.onExitSuccess(handlers.onExitSuccess);
82
- if (handlers.onExitAbort) connector.onExitAbort(handlers.onExitAbort);
83
- if (handlers.onExitError) connector.onExitError(handlers.onExitError);
84
- return ()=>{
85
- if (handlers.onEvent) connector.offEvent(handlers.onEvent);
86
- if (handlers.onOpen) connector.offOpen(handlers.onOpen);
87
- if (handlers.onLoad) connector.offLoad(handlers.onLoad);
88
- if (handlers.onExit) connector.offExit(handlers.onExit);
89
- if (handlers.onExitSuccess) connector.offExitSuccess(handlers.onExitSuccess);
90
- if (handlers.onExitAbort) connector.offExitAbort(handlers.onExitAbort);
91
- if (handlers.onExitError) connector.offExitError(handlers.onExitError);
92
- };
93
- }, [
94
- connector,
95
- options?.onEvent,
96
- options?.onOpen,
97
- options?.onLoad,
98
- options?.onExit,
99
- options?.onExitSuccess,
100
- options?.onExitAbort,
101
- options?.onExitError
102
- ]);
103
- // This is used to hide any potential race conditions from usage; allowing
104
- // interaction before the script may have loaded.
105
- useEffect(()=>{
106
- if (connector && isOpening) {
107
- setIsOpening(false);
108
- connector.open();
109
- }
110
- }, [
111
- connector,
112
- isOpening
113
- ]);
114
- const open = useCallback(()=>{
115
- if (connectorId) {
116
- setIsOpening(true);
117
- } else {
118
- throw new Error('Must provide `connectorId` to `open` Quiltt Connector with Method Call');
119
- }
120
- }, [
121
- connectorId
122
- ]);
123
- return {
124
- open
125
- };
126
- };
127
-
128
- export { useQuilttConnector as u, version as v };