@quiltt/react 4.3.1 → 4.3.3

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,10 +1,29 @@
1
1
  # @quiltt/react
2
2
 
3
+ ## 4.3.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#375](https://github.com/quiltt/quiltt-js/pull/375) [`fdc91e3`](https://github.com/quiltt/quiltt-js/commit/fdc91e3efb3f63659580f2d1d2ea0ff7fcaee8f5) Thanks [@sirwolfgang](https://github.com/sirwolfgang)! - Add 403 callback to Auth useIdentifySession
8
+
9
+ - Updated dependencies [[`fdc91e3`](https://github.com/quiltt/quiltt-js/commit/fdc91e3efb3f63659580f2d1d2ea0ff7fcaee8f5)]:
10
+ - @quiltt/core@4.3.3
11
+
12
+ ## 4.3.2
13
+
14
+ ### Patch Changes
15
+
16
+ - [#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.
17
+
18
+ - Updated dependencies [[`c022ebe`](https://github.com/quiltt/quiltt-js/commit/c022ebed0c82404a1bdbf5abbaf6e60b49f2d07a)]:
19
+ - @quiltt/core@4.3.2
20
+
3
21
  ## 4.3.1
4
22
 
5
23
  ### Patch Changes
6
24
 
7
25
  - [#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
26
+ - [#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
8
27
 
9
28
  - Updated dependencies [[`dc376b5`](https://github.com/quiltt/quiltt-js/commit/dc376b52dd824d7867ca74677bbfd5c54eff5cdc)]:
10
29
  - @quiltt/core@4.3.1
@@ -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-CM6ALGSN.js';
10
+ import { u as useQuilttSession } from './useQuilttSession-12s-BCq3OL9S.js';
11
11
 
12
12
  const useAuthenticateSession = (auth, setSession)=>{
13
13
  const authenticateSession = useCallback(async (payload, callbacks)=>{
@@ -44,6 +44,9 @@ const useIdentifySession = (auth, setSession)=>{
44
44
  case 202:
45
45
  if (callbacks.onChallenged) return callbacks.onChallenged();
46
46
  break;
47
+ case 403:
48
+ if (callbacks.onForbidden) return callbacks.onForbidden();
49
+ break;
47
50
  case 422:
48
51
  if (callbacks.onError) return callbacks.onError(response.data);
49
52
  break;
@@ -133,8 +136,9 @@ const useRevokeSession = (auth, session, setSession)=>{
133
136
  }
134
137
  if (obj1 instanceof Set && obj2 instanceof Set) {
135
138
  if (obj1.size !== obj2.size) return false;
139
+ const arr2 = Array.from(obj2);
136
140
  for (const item of obj1){
137
- if (!Array.from(obj2).some((value)=>isDeepEqual(item, value))) return false;
141
+ if (!arr2.some((value)=>isDeepEqual(item, value))) return false;
138
142
  }
139
143
  return true;
140
144
  }
@@ -161,15 +165,22 @@ const useRevokeSession = (auth, session, setSession)=>{
161
165
  */ const QuilttAuthProvider = ({ graphqlClient, token, children })=>{
162
166
  const { session, importSession } = useQuilttSession();
163
167
  const previousSessionRef = useRef(session);
168
+ const previousTokenRef = useRef();
164
169
  // Memoize the client to avoid unnecessary re-renders
165
170
  const apolloClient = useMemo(()=>graphqlClient || new QuilttClient({
166
171
  cache: new InMemoryCache()
167
172
  }), [
168
173
  graphqlClient
169
174
  ]);
170
- // Import passed in token
175
+ // Import passed in token (only if value has changed)
171
176
  useEffect(()=>{
172
- if (token) importSession(token);
177
+ if (token && token !== previousTokenRef.current) {
178
+ importSession(token);
179
+ previousTokenRef.current = token;
180
+ } else if (!token) {
181
+ // Reset ref when token becomes undefined to allow re-import of same token later
182
+ previousTokenRef.current = undefined;
183
+ }
173
184
  }, [
174
185
  token,
175
186
  importSession
package/dist/index.d.ts CHANGED
@@ -94,6 +94,7 @@ type IdentifySessionCallbacks = {
94
94
  onSuccess?: () => unknown;
95
95
  onChallenged?: () => unknown;
96
96
  onError?: (errors: UnprocessableData) => unknown;
97
+ onForbidden?: () => unknown;
97
98
  };
98
99
  type IdentifySession = (payload: UsernamePayload, callbacks: IdentifySessionCallbacks) => Promise<unknown>;
99
100
  type UseIdentifySession = (auth: AuthAPI, setSession: SetSession) => IdentifySession;
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-CL6uHdqe.js';
5
- import { i as isDeepEqual, Q as QuilttAuthProvider } from './QuilttAuthProvider-12s-BZuGySu0.js';
6
- export { b as useAuthenticateSession, a as useIdentifySession, u as useImportSession, c as useRevokeSession } from './QuilttAuthProvider-12s-BZuGySu0.js';
4
+ import { u as useQuilttConnector } from './useQuilttConnector-12s-DTl-EiqV.js';
5
+ import { i as isDeepEqual, Q as QuilttAuthProvider } from './QuilttAuthProvider-12s-4hQ7iysR.js';
6
+ export { b as useAuthenticateSession, a as useIdentifySession, u as useImportSession, c as useRevokeSession } from './QuilttAuthProvider-12s-4hQ7iysR.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-FLdw-CQZ.js';
11
- export { u as useQuilttSession } from './useQuilttSession-12s-CM6ALGSN.js';
10
+ export { u as useQuilttInstitutions } from './useQuilttInstitutions-12s-Cg4OA77c.js';
11
+ export { u as useQuilttSession } from './useQuilttSession-12s-BCq3OL9S.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);
@@ -1,10 +1,11 @@
1
1
  'use client';
2
2
  import { useState, useRef, useEffect, useCallback } from 'react';
3
3
  import { cdnBase } from '@quiltt/core';
4
- import { u as useQuilttSession } from './useQuilttSession-12s-CM6ALGSN.js';
4
+ import { u as useQuilttSession } from './useQuilttSession-12s-BCq3OL9S.js';
5
5
  import { u as useScript } from './useScript-12s-JCgaTW9n.js';
6
+ import { i as isDeepEqual } from './QuilttAuthProvider-12s-4hQ7iysR.js';
6
7
 
7
- var version = "4.3.1";
8
+ var version = "4.3.3";
8
9
 
9
10
  const useQuilttConnector = (connectorId, options)=>{
10
11
  const status = useScript(`${cdnBase}/v1/connector.js?agent=react-${version}`, {
@@ -18,9 +19,15 @@ const useQuilttConnector = (connectorId, options)=>{
18
19
  // Keep track of the previous connectionId to detect changes
19
20
  const prevConnectionIdRef = useRef(options?.connectionId);
20
21
  const prevConnectorIdRef = useRef(connectorId);
22
+ const prevInstitutionRef = useRef(options?.institution);
21
23
  const connectorCreatedRef = useRef(false);
22
24
  // Track whether the connector is currently open
23
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
+ });
24
31
  // Set Session
25
32
  // biome-ignore lint/correctness/useExhaustiveDependencies: trigger effects when script status changes too
26
33
  useEffect(()=>{
@@ -36,10 +43,11 @@ const useQuilttConnector = (connectorId, options)=>{
36
43
  if (typeof Quiltt === 'undefined' || !connectorId) return;
37
44
  const currentConnectionId = options?.connectionId;
38
45
  const currentInstitution = options?.institution;
39
- // Check for changes
46
+ // Check for changes - use deep equality for institution object
40
47
  const connectionIdChanged = prevConnectionIdRef.current !== currentConnectionId;
41
48
  const connectorIdChanged = prevConnectorIdRef.current !== connectorId;
42
- const hasChanges = connectionIdChanged || connectorIdChanged || !connectorCreatedRef.current;
49
+ const institutionChanged = !isDeepEqual(prevInstitutionRef.current, currentInstitution);
50
+ const hasChanges = connectionIdChanged || connectorIdChanged || institutionChanged || !connectorCreatedRef.current;
43
51
  // Update if there are changes, regardless of what the changes are
44
52
  if (hasChanges) {
45
53
  if (currentConnectionId) {
@@ -57,6 +65,7 @@ const useQuilttConnector = (connectorId, options)=>{
57
65
  connectorCreatedRef.current = true;
58
66
  prevConnectionIdRef.current = currentConnectionId;
59
67
  prevConnectorIdRef.current = connectorId;
68
+ prevInstitutionRef.current = currentInstitution;
60
69
  }
61
70
  }, [
62
71
  connectorId,
@@ -64,56 +73,69 @@ const useQuilttConnector = (connectorId, options)=>{
64
73
  options?.institution,
65
74
  status
66
75
  ]);
67
- // Internal handlers to track connector state
76
+ // Internal handlers to track connector state (stable references)
68
77
  const handleOpen = useCallback((metadata)=>{
69
78
  isConnectorOpenRef.current = true;
70
- options?.onOpen?.(metadata);
71
- }, [
72
- options?.onOpen
73
- ]);
79
+ callbacksRef.current?.onOpen?.(metadata);
80
+ }, []);
74
81
  const handleExit = useCallback((type, metadata)=>{
75
82
  isConnectorOpenRef.current = false;
76
- options?.onExit?.(type, metadata);
77
- }, [
78
- options?.onExit
79
- ]);
80
- // Register event handlers
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)
81
102
  useEffect(()=>{
82
103
  if (!connector) return;
83
- const handlers = {
84
- onEvent: options?.onEvent,
85
- onOpen: handleOpen,
86
- onLoad: options?.onLoad,
87
- onExit: handleExit,
88
- onExitSuccess: options?.onExitSuccess,
89
- onExitAbort: options?.onExitAbort,
90
- onExitError: options?.onExitError
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
91
113
  };
92
- if (handlers.onEvent) connector.onEvent(handlers.onEvent);
93
- if (handlers.onOpen) connector.onOpen(handlers.onOpen);
94
- if (handlers.onLoad) connector.onLoad(handlers.onLoad);
95
- if (handlers.onExit) connector.onExit(handlers.onExit);
96
- if (handlers.onExitSuccess) connector.onExitSuccess(handlers.onExitSuccess);
97
- if (handlers.onExitAbort) connector.onExitAbort(handlers.onExitAbort);
98
- if (handlers.onExitError) connector.onExitError(handlers.onExitError);
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);
99
121
  return ()=>{
100
- if (handlers.onEvent) connector.offEvent(handlers.onEvent);
101
- if (handlers.onOpen) connector.offOpen(handlers.onOpen);
102
- if (handlers.onLoad) connector.offLoad(handlers.onLoad);
103
- if (handlers.onExit) connector.offExit(handlers.onExit);
104
- if (handlers.onExitSuccess) connector.offExitSuccess(handlers.onExitSuccess);
105
- if (handlers.onExitAbort) connector.offExitAbort(handlers.onExitAbort);
106
- if (handlers.onExitError) connector.offExitError(handlers.onExitError);
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);
107
129
  };
108
130
  }, [
109
131
  connector,
110
- options?.onEvent,
132
+ stableOnEvent,
111
133
  handleOpen,
112
- options?.onLoad,
134
+ stableOnLoad,
113
135
  handleExit,
114
- options?.onExitSuccess,
115
- options?.onExitAbort,
116
- options?.onExitError
136
+ stableOnExitSuccess,
137
+ stableOnExitAbort,
138
+ stableOnExitError
117
139
  ]);
118
140
  // This is used to hide any potential race conditions from usage; allowing
119
141
  // interaction before the script may have loaded.
@@ -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-CL6uHdqe.js';
5
+ import { v as version } from './useQuilttConnector-12s-DTl-EiqV.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-BZuGySu0.js';
4
+ import { u as useImportSession, a as useIdentifySession, b as useAuthenticateSession, c as useRevokeSession } from './QuilttAuthProvider-12s-4hQ7iysR.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.1",
3
+ "version": "4.3.3",
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.1"
39
+ "@quiltt/core": "4.3.3"
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
@@ -14,6 +14,7 @@ type IdentifySessionCallbacks = {
14
14
  onSuccess?: () => unknown
15
15
  onChallenged?: () => unknown
16
16
  onError?: (errors: UnprocessableData) => unknown
17
+ onForbidden?: () => unknown
17
18
  }
18
19
  export type IdentifySession = (
19
20
  payload: UsernamePayload,
@@ -37,6 +38,10 @@ export const useIdentifySession: UseIdentifySession = (auth, setSession) => {
37
38
  if (callbacks.onChallenged) return callbacks.onChallenged()
38
39
  break
39
40
 
41
+ case 403: // Forbidden (signups disabled)
42
+ if (callbacks.onForbidden) return callbacks.onForbidden()
43
+ break
44
+
40
45
  case 422: // Unprocessable Content
41
46
  if (callbacks.onError) return callbacks.onError((response as UnprocessableResponse).data)
42
47
  break
@@ -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,11 +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
 
38
40
  // Track whether the connector is currently open
39
41
  const isConnectorOpenRef = useRef<boolean>(false)
40
42
 
43
+ // Store callbacks in refs to maintain stable references
44
+ const callbacksRef = useRef<ConnectorSDKConnectorOptions>(options || {})
45
+ useEffect(() => {
46
+ callbacksRef.current = options || {}
47
+ })
48
+
41
49
  // Set Session
42
50
  // biome-ignore lint/correctness/useExhaustiveDependencies: trigger effects when script status changes too
43
51
  useEffect(() => {
@@ -54,10 +62,15 @@ export const useQuilttConnector = (
54
62
  const currentConnectionId = options?.connectionId
55
63
  const currentInstitution = options?.institution
56
64
 
57
- // Check for changes
65
+ // Check for changes - use deep equality for institution object
58
66
  const connectionIdChanged = prevConnectionIdRef.current !== currentConnectionId
59
67
  const connectorIdChanged = prevConnectorIdRef.current !== connectorId
60
- 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
61
74
 
62
75
  // Update if there are changes, regardless of what the changes are
63
76
  if (hasChanges) {
@@ -73,66 +86,83 @@ export const useQuilttConnector = (
73
86
  connectorCreatedRef.current = true
74
87
  prevConnectionIdRef.current = currentConnectionId
75
88
  prevConnectorIdRef.current = connectorId
89
+ prevInstitutionRef.current = currentInstitution
76
90
  }
77
91
  }, [connectorId, options?.connectionId, options?.institution, status])
78
92
 
79
- // Internal handlers to track connector state
80
- const handleOpen = useCallback(
81
- (metadata: any) => {
82
- isConnectorOpenRef.current = true
83
- options?.onOpen?.(metadata)
84
- },
85
- [options?.onOpen]
86
- )
87
-
88
- const handleExit = useCallback(
89
- (type: any, metadata: any) => {
90
- isConnectorOpenRef.current = false
91
- options?.onExit?.(type, metadata)
92
- },
93
- [options?.onExit]
94
- )
95
-
96
- // 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)
97
126
  useEffect(() => {
98
127
  if (!connector) return
99
128
 
100
- const handlers = {
101
- onEvent: options?.onEvent,
102
- onOpen: handleOpen,
103
- onLoad: options?.onLoad,
104
- onExit: handleExit,
105
- onExitSuccess: options?.onExitSuccess,
106
- onExitAbort: options?.onExitAbort,
107
- 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,
108
138
  }
109
139
 
110
- if (handlers.onEvent) connector.onEvent(handlers.onEvent)
111
- if (handlers.onOpen) connector.onOpen(handlers.onOpen)
112
- if (handlers.onLoad) connector.onLoad(handlers.onLoad)
113
- if (handlers.onExit) connector.onExit(handlers.onExit)
114
- if (handlers.onExitSuccess) connector.onExitSuccess(handlers.onExitSuccess)
115
- if (handlers.onExitAbort) connector.onExitAbort(handlers.onExitAbort)
116
- 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)
117
147
 
118
148
  return () => {
119
- if (handlers.onEvent) connector.offEvent(handlers.onEvent)
120
- if (handlers.onOpen) connector.offOpen(handlers.onOpen)
121
- if (handlers.onLoad) connector.offLoad(handlers.onLoad)
122
- if (handlers.onExit) connector.offExit(handlers.onExit)
123
- if (handlers.onExitSuccess) connector.offExitSuccess(handlers.onExitSuccess)
124
- if (handlers.onExitAbort) connector.offExitAbort(handlers.onExitAbort)
125
- 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)
126
156
  }
127
157
  }, [
128
158
  connector,
129
- options?.onEvent,
159
+ stableOnEvent,
130
160
  handleOpen,
131
- options?.onLoad,
161
+ stableOnLoad,
132
162
  handleExit,
133
- options?.onExitSuccess,
134
- options?.onExitAbort,
135
- options?.onExitError,
163
+ stableOnExitSuccess,
164
+ stableOnExitAbort,
165
+ stableOnExitError,
136
166
  ])
137
167
 
138
168
  // This is used to hide any potential race conditions from usage; allowing
@@ -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
  })
@@ -29,6 +29,7 @@ export const QuilttAuthProvider: FC<QuilttAuthProviderProps> = ({
29
29
  }) => {
30
30
  const { session, importSession } = useQuilttSession()
31
31
  const previousSessionRef = useRef(session)
32
+ const previousTokenRef = useRef<string | undefined>()
32
33
 
33
34
  // Memoize the client to avoid unnecessary re-renders
34
35
  const apolloClient = useMemo(
@@ -40,9 +41,15 @@ export const QuilttAuthProvider: FC<QuilttAuthProviderProps> = ({
40
41
  [graphqlClient]
41
42
  )
42
43
 
43
- // Import passed in token
44
+ // Import passed in token (only if value has changed)
44
45
  useEffect(() => {
45
- 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
+ }
46
53
  }, [token, importSession])
47
54
 
48
55
  // Reset Client Store when session changes (using deep comparison)
@@ -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
  }