@quiltt/react 4.3.3 → 4.5.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,59 @@
1
1
  # @quiltt/react
2
2
 
3
+ ## 4.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#386](https://github.com/quiltt/quiltt-js/pull/386) [`0bf706c`](https://github.com/quiltt/quiltt-js/commit/0bf706ce2ad926304d6eac739ee58971736f913e) Thanks [@zubairaziz](https://github.com/zubairaziz)! - Update platform webview props
8
+
9
+ ### Patch Changes
10
+
11
+ - [#380](https://github.com/quiltt/quiltt-js/pull/380) [`31cd190`](https://github.com/quiltt/quiltt-js/commit/31cd1902618ebc2314d42dd7aca81b3ab94068ea) Thanks [@sirwolfgang](https://github.com/sirwolfgang)! - Improve useQuilttResolvable error messaging
12
+
13
+ - Updated dependencies [[`31cd190`](https://github.com/quiltt/quiltt-js/commit/31cd1902618ebc2314d42dd7aca81b3ab94068ea), [`0bf706c`](https://github.com/quiltt/quiltt-js/commit/0bf706ce2ad926304d6eac739ee58971736f913e)]:
14
+ - @quiltt/core@4.5.0
15
+
16
+ ## 4.4.0
17
+
18
+ ### Minor Changes
19
+
20
+ - [#378](https://github.com/quiltt/quiltt-js/pull/378) [`0af4e66`](https://github.com/quiltt/quiltt-js/commit/0af4e6622d1542e0c0c02ac7e897e3e4f9219cbd) Thanks [@sirwolfgang](https://github.com/sirwolfgang)! - Add connector institution search and provider migration support.
21
+
22
+ ## New APIs
23
+
24
+ ### `useQuilttResolvable` Hook
25
+
26
+ Check if external provider institution IDs (e.g., Plaid) can be migrated to your connector.
27
+
28
+ ```typescript
29
+ import { useQuilttResolvable } from "@quiltt/react";
30
+ import { useEffect } from "react";
31
+
32
+ function ResolvableConnector({ plaidInstitutionId, children }) {
33
+ const { checkResolvable, isResolvable, isLoading } =
34
+ useQuilttResolvable("my-connector-id");
35
+
36
+ useEffect(() => {
37
+ checkResolvable({ plaid: plaidInstitutionId });
38
+ }, [plaidInstitutionId]);
39
+
40
+ if (isLoading) return <div>Checking...</div>;
41
+ if (!isResolvable) return null;
42
+
43
+ return <>{children}</>;
44
+ }
45
+
46
+ // Usage
47
+ <ResolvableConnector plaidInstitutionId="ins_3">
48
+ <QuilttButton connectorId="my-connector-id" />
49
+ </ResolvableConnector>;
50
+ ```
51
+
52
+ ### Patch Changes
53
+
54
+ - Updated dependencies [[`0af4e66`](https://github.com/quiltt/quiltt-js/commit/0af4e6622d1542e0c0c02ac7e897e3e4f9219cbd)]:
55
+ - @quiltt/core@4.4.0
56
+
3
57
  ## 4.3.3
4
58
 
5
59
  ### Patch Changes
@@ -5,9 +5,10 @@ import '@apollo/client/react/hooks/useApolloClient.js';
5
5
  import './QuilttSettings-12s-BK-0SQME.js';
6
6
  import './useSession-12s-7GOn4sUn.js';
7
7
  import 'use-debounce';
8
+ import './QuilttProviderRender-12s-DtQtubjL.js';
8
9
  import { jsx } from 'react/jsx-runtime';
9
10
  import { ApolloProvider } from '@apollo/client/react/context/ApolloProvider.js';
10
- import { u as useQuilttSession } from './useQuilttSession-12s-BCq3OL9S.js';
11
+ import { u as useQuilttSession } from './useQuilttSession-12s-D10t5Wfh.js';
11
12
 
12
13
  const useAuthenticateSession = (auth, setSession)=>{
13
14
  const authenticateSession = useCallback(async (payload, callbacks)=>{
@@ -0,0 +1,6 @@
1
+ 'use client';
2
+ import { createContext } from 'react';
3
+
4
+ const QuilttProviderRender = createContext({});
5
+
6
+ export { QuilttProviderRender as Q };
package/dist/index.d.ts CHANGED
@@ -125,6 +125,37 @@ type UseQuilttInstitutions = (connectorId: string, onErrorCallback?: (msg: strin
125
125
  };
126
126
  declare const useQuilttInstitutions: UseQuilttInstitutions;
127
127
 
128
+ /**
129
+ * Internal hook that detects when a Quiltt SDK component may be rendered
130
+ * in the same component as QuilttProvider, which is an anti-pattern that
131
+ * can cause memory context issues and unexpected behavior.
132
+ *
133
+ * **Limitation**: Due to React context propagation, this will trigger for ALL
134
+ * descendants of QuilttProvider, not just direct children. This means it may
135
+ * produce false positives for valid nested component patterns. However, the
136
+ * primary use case (same-component rendering) is reliably detected.
137
+ *
138
+ * When the flag is set, this hook will emit a console error. This primarily
139
+ * helps catch the anti-pattern but may also flag valid nested structures.
140
+ *
141
+ * @param componentName - The name of the component calling this hook (for error messages)
142
+ */
143
+ declare const useQuilttRenderGuard: (componentName: string) => void;
144
+
145
+ type UseQuilttResolvable = (connectorId: string, onErrorCallback?: (msg: string) => void) => {
146
+ checkResolvable: (providerId: {
147
+ plaid?: string;
148
+ mock?: string;
149
+ mx?: string;
150
+ finicity?: string;
151
+ akoya?: string;
152
+ }) => Promise<boolean | null>;
153
+ isLoading: boolean;
154
+ isResolvable: boolean | null;
155
+ error: string | null;
156
+ };
157
+ declare const useQuilttResolvable: UseQuilttResolvable;
158
+
128
159
  type UseQuilttSession = (environmentId?: string) => {
129
160
  session: Maybe<QuilttJWT> | undefined;
130
161
  importSession: ImportSession;
@@ -182,5 +213,5 @@ declare const QuilttSettingsProvider: FC<QuilttSettingsProviderProps>;
182
213
  type QuilttProviderProps = QuilttSettingsProviderProps & QuilttAuthProviderProps;
183
214
  declare const QuilttProvider: FC<QuilttProviderProps>;
184
215
 
185
- export { QuilttAuthProvider, QuilttButton, QuilttContainer, QuilttProvider, QuilttSettingsProvider, useAuthenticateSession, useEventListener, useIdentifySession, useImportSession, useIsomorphicLayoutEffect, useQuilttClient, useQuilttConnector, useQuilttInstitutions, useQuilttSession, useQuilttSettings, useRevokeSession, useSession, useStorage };
186
- export type { AuthenticateSession, IdentifySession, ImportSession, QuilttAuthProviderProps, QuilttSettingsProviderProps, RevokeSession, SetSession, UseQuilttInstitutions, UseQuilttSession };
216
+ export { QuilttAuthProvider, QuilttButton, QuilttContainer, QuilttProvider, QuilttSettingsProvider, useAuthenticateSession, useEventListener, useIdentifySession, useImportSession, useIsomorphicLayoutEffect, useQuilttClient, useQuilttConnector, useQuilttInstitutions, useQuilttRenderGuard, useQuilttResolvable, useQuilttSession, useQuilttSettings, useRevokeSession, useSession, useStorage };
217
+ export type { AuthenticateSession, IdentifySession, ImportSession, QuilttAuthProviderProps, QuilttSettingsProviderProps, RevokeSession, SetSession, UseQuilttInstitutions, UseQuilttResolvable, UseQuilttSession };
package/dist/index.js CHANGED
@@ -1,17 +1,20 @@
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-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';
4
+ import { u as useQuilttConnector } from './useQuilttConnector-12s-7jon0A8C.js';
5
+ import { u as useQuilttRenderGuard } from './useQuilttRenderGuard-12s-BbUitnF1.js';
6
+ import { i as isDeepEqual, Q as QuilttAuthProvider } from './QuilttAuthProvider-12s-BCpwSX-3.js';
7
+ export { b as useAuthenticateSession, a as useIdentifySession, u as useImportSession, c as useRevokeSession } from './QuilttAuthProvider-12s-BCpwSX-3.js';
7
8
  export { u as useEventListener } from './useEventListener-12s-D_-6QIXa.js';
8
9
  export { u as useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect-12s-DeTHOKz1.js';
9
10
  export { u as useQuilttClient } from './useQuilttClient-12s-CAAUait1.js';
10
- export { u as useQuilttInstitutions } from './useQuilttInstitutions-12s-Cg4OA77c.js';
11
- export { u as useQuilttSession } from './useQuilttSession-12s-BCq3OL9S.js';
11
+ export { u as useQuilttInstitutions } from './useQuilttInstitutions-12s-Ca5smTKo.js';
12
+ export { u as useQuilttResolvable } from './useQuilttResolvable-12s-CNvWZe0V.js';
13
+ export { u as useQuilttSession } from './useQuilttSession-12s-D10t5Wfh.js';
12
14
  export { u as useQuilttSettings } from './useQuilttSettings-12s--rCJoNHD.js';
13
15
  export { u as useSession } from './useSession-12s-7GOn4sUn.js';
14
16
  export { u as useStorage } from './useStorage-12s-DHcq3Kuh.js';
17
+ import { Q as QuilttProviderRender } from './QuilttProviderRender-12s-DtQtubjL.js';
15
18
  import { Q as QuilttSettingsProvider } from './QuilttSettingsProvider-12s-ZcmFmOiZ.js';
16
19
 
17
20
  /**
@@ -21,6 +24,8 @@ import { Q as QuilttSettingsProvider } from './QuilttSettingsProvider-12s-ZcmFmO
21
24
  * connector instance with the new connection details. If you need to force a
22
25
  * complete remount instead, set forceRemountOnConnectionChange to true.
23
26
  */ const QuilttButton = ({ as, connectorId, connectionId, institution, forceRemountOnConnectionChange = false, onEvent, onOpen, onLoad, onExit, onExitSuccess, onExitAbort, onExitError, onClick, onHtmlLoad, children, ...props })=>{
27
+ // Check flag to warn about potential anti-pattern (may produce false positives for valid nested patterns)
28
+ useQuilttRenderGuard('QuilttButton');
24
29
  // Keep track of previous connectionId for change detection
25
30
  const prevConnectionIdRef = useRef(connectionId);
26
31
  const prevCallbacksRef = useRef({
@@ -103,6 +108,8 @@ import { Q as QuilttSettingsProvider } from './QuilttSettingsProvider-12s-ZcmFmO
103
108
  * connector instance with the new connection details. If you need to force a
104
109
  * complete remount instead, set forceRemountOnConnectionChange to true.
105
110
  */ const QuilttContainer = ({ as, connectorId, connectionId, forceRemountOnConnectionChange = false, onEvent, onLoad, onExit, onExitSuccess, onExitAbort, onExitError, children, ...props })=>{
111
+ // Check flag to warn about potential anti-pattern (may produce false positives for valid nested patterns)
112
+ useQuilttRenderGuard('QuilttContainer');
106
113
  // Keep track of previous connectionId for change detection
107
114
  const prevConnectionIdRef = useRef(connectionId);
108
115
  const prevCallbacksRef = useRef({
@@ -162,14 +169,24 @@ import { Q as QuilttSettingsProvider } from './QuilttSettingsProvider-12s-ZcmFmO
162
169
  };
163
170
 
164
171
  const QuilttProvider = ({ clientId, graphqlClient, token, children })=>{
165
- return /*#__PURE__*/ jsx(QuilttSettingsProvider, {
166
- clientId: clientId,
167
- children: /*#__PURE__*/ jsx(QuilttAuthProvider, {
168
- token: token,
169
- graphqlClient: graphqlClient,
170
- children: children
172
+ // Set a context flag that SDK components can check to warn about potential anti-patterns.
173
+ // LIMITATION: This flags ALL descendants due to React context propagation, not just same-component usage.
174
+ // Will produce false positives for valid patterns like: <QuilttProvider><MyPage /></QuilttProvider>
175
+ // where MyPage renders SDK components (which is correct usage).
176
+ // The flag-based approach is simple but imprecise - a proper solution would require render stack tracking.
177
+ return /*#__PURE__*/ jsx(QuilttProviderRender.Provider, {
178
+ value: {
179
+ isRenderingProvider: true
180
+ },
181
+ children: /*#__PURE__*/ jsx(QuilttSettingsProvider, {
182
+ clientId: clientId,
183
+ children: /*#__PURE__*/ jsx(QuilttAuthProvider, {
184
+ token: token,
185
+ graphqlClient: graphqlClient,
186
+ children: children
187
+ })
171
188
  })
172
189
  });
173
190
  };
174
191
 
175
- export { QuilttAuthProvider, QuilttButton, QuilttContainer, QuilttProvider, QuilttSettingsProvider, useQuilttConnector };
192
+ export { QuilttAuthProvider, QuilttButton, QuilttContainer, QuilttProvider, QuilttSettingsProvider, useQuilttConnector, useQuilttRenderGuard };
@@ -1,11 +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-BCq3OL9S.js';
4
+ import { u as useQuilttSession } from './useQuilttSession-12s-D10t5Wfh.js';
5
5
  import { u as useScript } from './useScript-12s-JCgaTW9n.js';
6
- import { i as isDeepEqual } from './QuilttAuthProvider-12s-4hQ7iysR.js';
6
+ import { i as isDeepEqual } from './QuilttAuthProvider-12s-BCpwSX-3.js';
7
7
 
8
- var version = "4.3.3";
8
+ var version = "4.5.0";
9
9
 
10
10
  const useQuilttConnector = (connectorId, options)=>{
11
11
  const status = useScript(`${cdnBase}/v1/connector.js?agent=react-${version}`, {
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
  import { useMemo, useState, useRef, useEffect, useCallback } from 'react';
3
- import { InstitutionsAPI } from '@quiltt/core';
3
+ import { ConnectorsAPI } from '@quiltt/core';
4
4
  import { useDebounce } from 'use-debounce';
5
- import { v as version } from './useQuilttConnector-12s-DTl-EiqV.js';
5
+ import { v as version } from './useQuilttConnector-12s-7jon0A8C.js';
6
6
  import { u as useSession } from './useSession-12s-7GOn4sUn.js';
7
7
 
8
8
  const useQuilttInstitutions = (connectorId, onErrorCallback)=>{
@@ -18,7 +18,7 @@ const useQuilttInstitutions = (connectorId, onErrorCallback)=>{
18
18
  typeof navigator !== 'undefined');
19
19
  return isReactNative ? `react-native-${version}` : `react-${version}`;
20
20
  }, []);
21
- const institutionsAPI = useMemo(()=>new InstitutionsAPI(connectorId, agent), [
21
+ const connectorsAPI = useMemo(()=>new ConnectorsAPI(connectorId, agent), [
22
22
  connectorId,
23
23
  agent
24
24
  ]);
@@ -58,7 +58,7 @@ const useQuilttInstitutions = (connectorId, onErrorCallback)=>{
58
58
  return;
59
59
  }
60
60
  const abortController = new AbortController();
61
- institutionsAPI.search(session?.token, connectorId, searchTerm, abortController.signal).then((response)=>{
61
+ connectorsAPI.searchInstitutions(session?.token, connectorId, searchTerm, abortController.signal).then((response)=>{
62
62
  if (!abortController.signal.aborted) {
63
63
  if (response.status === 200) {
64
64
  setSearchResults(response.data);
@@ -78,7 +78,7 @@ const useQuilttInstitutions = (connectorId, onErrorCallback)=>{
78
78
  session?.token,
79
79
  connectorId,
80
80
  searchTerm,
81
- institutionsAPI,
81
+ connectorsAPI,
82
82
  handleError
83
83
  ]);
84
84
  return {
@@ -0,0 +1,37 @@
1
+ 'use client';
2
+ import { useContext, useRef, useEffect } from 'react';
3
+ import { Q as QuilttProviderRender } from './QuilttProviderRender-12s-DtQtubjL.js';
4
+
5
+ /**
6
+ * Internal hook that detects when a Quiltt SDK component may be rendered
7
+ * in the same component as QuilttProvider, which is an anti-pattern that
8
+ * can cause memory context issues and unexpected behavior.
9
+ *
10
+ * **Limitation**: Due to React context propagation, this will trigger for ALL
11
+ * descendants of QuilttProvider, not just direct children. This means it may
12
+ * produce false positives for valid nested component patterns. However, the
13
+ * primary use case (same-component rendering) is reliably detected.
14
+ *
15
+ * When the flag is set, this hook will emit a console error. This primarily
16
+ * helps catch the anti-pattern but may also flag valid nested structures.
17
+ *
18
+ * @param componentName - The name of the component calling this hook (for error messages)
19
+ */ const useQuilttRenderGuard = (componentName)=>{
20
+ const { isRenderingProvider } = useContext(QuilttProviderRender);
21
+ const hasWarnedRef = useRef(false);
22
+ useEffect(()=>{
23
+ // Only run in development mode and warn once per component instance
24
+ // Support both Node.js (process.env) and Vite (import.meta.env) environments
25
+ const isProduction = process.env.NODE_ENV === 'production' || typeof import.meta !== 'undefined' && import.meta.env?.PROD;
26
+ if (isProduction) return;
27
+ if (isRenderingProvider && !hasWarnedRef.current) {
28
+ hasWarnedRef.current = true;
29
+ console.error(`[Quiltt] ⚠️ POTENTIAL ANTI-PATTERN: ${componentName} may be rendered in the same component as QuilttProvider.\n\n` + `NOTE: This check uses a simple flag and may produce false positives for valid nested patterns.\n` + `If ${componentName} is in a SEPARATE component from QuilttProvider, you can safely ignore this.\n\n` + `The ANTI-PATTERN we're trying to catch:\n` + ` • Rendering Provider and SDK components in the SAME function component\n` + ` • This causes memory context issues because they run at the same React execution layer\n` + ` • React cannot properly manage the context hierarchy in this case\n\n` + `RECOMMENDED PATTERN:\n` + ` • Move QuilttProvider to a parent component or layout\n` + ` • Render ${componentName} in a child component\n\n` + `Example:\n\n` + ` // ✅ CORRECT: Provider in parent, SDK component in child (separate components)\n` + ` function Providers({ children }) {\n` + ` return (\n` + ` <QuilttProvider token={token}>\n` + ` {children}\n` + ` </QuilttProvider>\n` + ` )\n` + ` }\n\n` + ` function MyFeature() {\n` + ` return <${componentName} connectorId="..." />\n` + ` }\n\n` + ` // ❌ ANTI-PATTERN: Both in SAME component\n` + ` function MyFeature() {\n` + ` return (\n` + ` <QuilttProvider token={token}>\n` + ` <${componentName} connectorId="..." />\n` + ` </QuilttProvider>\n` + ` )\n` + ` }\n\n` + `Learn more:\n` + ` • https://github.com/quiltt/quiltt-js/tree/main/packages/react#provider-usage\n` + ` • https://react.dev/reference/react/useContext#my-component-doesnt-see-the-value-from-my-provider`);
30
+ }
31
+ }, [
32
+ isRenderingProvider,
33
+ componentName
34
+ ]);
35
+ };
36
+
37
+ export { useQuilttRenderGuard as u };
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+ import { useMemo, useState, useRef, useEffect, useCallback } from 'react';
3
+ import { ConnectorsAPI } from '@quiltt/core';
4
+ import { v as version } from './useQuilttConnector-12s-7jon0A8C.js';
5
+ import { u as useSession } from './useSession-12s-7GOn4sUn.js';
6
+
7
+ const useQuilttResolvable = (connectorId, onErrorCallback)=>{
8
+ const agent = useMemo(()=>{
9
+ // Try deprecated navigator.product first (still used in some RN versions)
10
+ if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
11
+ return `react-native-${version}`;
12
+ }
13
+ // Detect React Native by its unique environment characteristics
14
+ const isReactNative = !!// Has window (unlike Node.js)
15
+ (typeof window !== 'undefined' && // No document in window (unlike browsers)
16
+ typeof window.document === 'undefined' && // Has navigator (unlike Node.js)
17
+ typeof navigator !== 'undefined');
18
+ return isReactNative ? `react-native-${version}` : `react-${version}`;
19
+ }, []);
20
+ const connectorsAPI = useMemo(()=>new ConnectorsAPI(connectorId, agent), [
21
+ connectorId,
22
+ agent
23
+ ]);
24
+ const [session] = useSession();
25
+ const [isLoading, setIsLoading] = useState(false);
26
+ const [isResolvable, setIsResolvable] = useState(null);
27
+ const [error, setError] = useState(null);
28
+ // Store callback in ref to maintain stable reference
29
+ const onErrorCallbackRef = useRef(onErrorCallback);
30
+ useEffect(()=>{
31
+ onErrorCallbackRef.current = onErrorCallback;
32
+ });
33
+ const handleError = useCallback((message)=>{
34
+ const errorMessage = message || 'Unknown error occurred while checking resolvability';
35
+ setError(errorMessage);
36
+ console.error('Quiltt Connector Resolvable Error:', errorMessage);
37
+ if (onErrorCallbackRef.current) onErrorCallbackRef.current(errorMessage);
38
+ }, []);
39
+ const checkResolvable = useCallback(async (providerId)=>{
40
+ if (!session?.token) {
41
+ handleError('Missing session token');
42
+ return null;
43
+ }
44
+ if (!connectorId) {
45
+ handleError('Missing connector ID');
46
+ return null;
47
+ }
48
+ const hasProviderId = Object.values(providerId).some((id)=>!!id);
49
+ if (!hasProviderId) {
50
+ handleError('No provider ID specified');
51
+ return null;
52
+ }
53
+ setIsLoading(true);
54
+ setError(null);
55
+ try {
56
+ const response = await connectorsAPI.checkResolvable(session.token, connectorId, providerId);
57
+ if (response.status === 200) {
58
+ const result = response.data.resolvable;
59
+ setIsResolvable(result);
60
+ return result;
61
+ }
62
+ handleError(response.data.message || 'Failed to check resolvability');
63
+ setIsResolvable(null);
64
+ return null;
65
+ } catch (error) {
66
+ handleError(error?.message);
67
+ setIsResolvable(null);
68
+ return null;
69
+ } finally{
70
+ setIsLoading(false);
71
+ }
72
+ }, [
73
+ session?.token,
74
+ connectorId,
75
+ connectorsAPI,
76
+ handleError
77
+ ]);
78
+ return {
79
+ checkResolvable,
80
+ isLoading,
81
+ isResolvable,
82
+ error
83
+ };
84
+ };
85
+
86
+ export { useQuilttResolvable as u };
@@ -1,14 +1,16 @@
1
1
  'use client';
2
- import { useCallback } from 'react';
2
+ import { useMemo, 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-4hQ7iysR.js';
4
+ import { u as useImportSession, a as useIdentifySession, b as useAuthenticateSession, c as useRevokeSession } from './QuilttAuthProvider-12s-BCpwSX-3.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
 
8
8
  const useQuilttSession = (environmentId)=>{
9
9
  const { clientId } = useQuilttSettings();
10
10
  const [session, setSession] = useSession();
11
- const auth = new AuthAPI(clientId);
11
+ const auth = useMemo(()=>new AuthAPI(clientId), [
12
+ clientId
13
+ ]);
12
14
  const importSession = useImportSession(auth, session, setSession, environmentId);
13
15
  const identifySession = useIdentifySession(auth, setSession);
14
16
  const authenticateSession = useAuthenticateSession(auth, setSession);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quiltt/react",
3
- "version": "4.3.3",
3
+ "version": "4.5.0",
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.3"
39
+ "@quiltt/core": "4.5.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@biomejs/biome": "2.2.4",
@@ -4,6 +4,7 @@ import { useEffect, useRef } from 'react'
4
4
  import type { ConnectorSDKCallbacks } from '@quiltt/core'
5
5
 
6
6
  import { useQuilttConnector } from '@/hooks/useQuilttConnector'
7
+ import { useQuilttRenderGuard } from '@/hooks/useQuilttRenderGuard'
7
8
  import type { PropsOf } from '@/types'
8
9
  import { isDeepEqual } from '@/utils/isDeepEqual'
9
10
 
@@ -62,6 +63,9 @@ export const QuilttButton = <T extends ElementType = 'button'>({
62
63
  children,
63
64
  ...props
64
65
  }: QuilttButtonProps<T> & PropsOf<T>) => {
66
+ // Check flag to warn about potential anti-pattern (may produce false positives for valid nested patterns)
67
+ useQuilttRenderGuard('QuilttButton')
68
+
65
69
  // Keep track of previous connectionId for change detection
66
70
  const prevConnectionIdRef = useRef<string | undefined>(connectionId)
67
71
  const prevCallbacksRef = useRef({
@@ -4,6 +4,7 @@ import { useEffect, useRef } from 'react'
4
4
  import type { ConnectorSDKCallbacks } from '@quiltt/core'
5
5
 
6
6
  import { useQuilttConnector } from '@/hooks/useQuilttConnector'
7
+ import { useQuilttRenderGuard } from '@/hooks/useQuilttRenderGuard'
7
8
  import type { PropsOf } from '@/types'
8
9
  import { isDeepEqual } from '@/utils/isDeepEqual'
9
10
 
@@ -44,6 +45,9 @@ export const QuilttContainer = <T extends ElementType = 'div'>({
44
45
  children,
45
46
  ...props
46
47
  }: QuilttContainerProps<T> & PropsOf<T>) => {
48
+ // Check flag to warn about potential anti-pattern (may produce false positives for valid nested patterns)
49
+ useQuilttRenderGuard('QuilttContainer')
50
+
47
51
  // Keep track of previous connectionId for change detection
48
52
  const prevConnectionIdRef = useRef<string | undefined>(connectionId)
49
53
  const prevCallbacksRef = useRef({
@@ -0,0 +1,15 @@
1
+ 'use client'
2
+
3
+ import { createContext } from 'react'
4
+
5
+ type QuilttProviderRenderContext = {
6
+ /**
7
+ * Flag indicating that QuilttProvider is in the ancestor tree.
8
+ * Due to React context propagation, this is true for ALL descendants,
9
+ * not just those in the same component as the provider.
10
+ * Used to detect potential anti-patterns in component composition.
11
+ */
12
+ isRenderingProvider?: boolean
13
+ }
14
+
15
+ export const QuilttProviderRender = createContext<QuilttProviderRenderContext>({})
@@ -3,6 +3,8 @@ export * from './session'
3
3
  export * from './useQuilttClient'
4
4
  export * from './useQuilttConnector'
5
5
  export * from './useQuilttInstitutions'
6
+ export * from './useQuilttRenderGuard'
7
+ export * from './useQuilttResolvable'
6
8
  export * from './useQuilttSession'
7
9
  export * from './useQuilttSettings'
8
10
  export * from './useSession'
@@ -3,7 +3,7 @@
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
4
4
 
5
5
  import type { ErrorData, InstitutionsData } from '@quiltt/core'
6
- import { InstitutionsAPI } from '@quiltt/core'
6
+ import { ConnectorsAPI } from '@quiltt/core'
7
7
  import { useDebounce } from 'use-debounce'
8
8
 
9
9
  import { version } from '../version'
@@ -41,10 +41,7 @@ export const useQuilttInstitutions: UseQuilttInstitutions = (connectorId, onErro
41
41
  return isReactNative ? `react-native-${version}` : `react-${version}`
42
42
  }, [])
43
43
 
44
- const institutionsAPI = useMemo(
45
- () => new InstitutionsAPI(connectorId, agent),
46
- [connectorId, agent]
47
- )
44
+ const connectorsAPI = useMemo(() => new ConnectorsAPI(connectorId, agent), [connectorId, agent])
48
45
  const [session] = useSession()
49
46
 
50
47
  const [searchTermInput, setSearchTermInput] = useState('')
@@ -93,8 +90,8 @@ export const useQuilttInstitutions: UseQuilttInstitutions = (connectorId, onErro
93
90
 
94
91
  const abortController = new AbortController()
95
92
 
96
- institutionsAPI
97
- .search(session?.token, connectorId, searchTerm, abortController.signal)
93
+ connectorsAPI
94
+ .searchInstitutions(session?.token, connectorId, searchTerm, abortController.signal)
98
95
  .then((response) => {
99
96
  if (!abortController.signal.aborted) {
100
97
  if (response.status === 200) {
@@ -113,7 +110,7 @@ export const useQuilttInstitutions: UseQuilttInstitutions = (connectorId, onErro
113
110
  })
114
111
 
115
112
  return () => abortController.abort()
116
- }, [session?.token, connectorId, searchTerm, institutionsAPI, handleError])
113
+ }, [session?.token, connectorId, searchTerm, connectorsAPI, handleError])
117
114
 
118
115
  return {
119
116
  searchTerm,
@@ -0,0 +1,76 @@
1
+ 'use client'
2
+
3
+ import { useContext, useEffect, useRef } from 'react'
4
+
5
+ import { QuilttProviderRender } from '@/contexts/QuilttProviderRender'
6
+
7
+ /**
8
+ * Internal hook that detects when a Quiltt SDK component may be rendered
9
+ * in the same component as QuilttProvider, which is an anti-pattern that
10
+ * can cause memory context issues and unexpected behavior.
11
+ *
12
+ * **Limitation**: Due to React context propagation, this will trigger for ALL
13
+ * descendants of QuilttProvider, not just direct children. This means it may
14
+ * produce false positives for valid nested component patterns. However, the
15
+ * primary use case (same-component rendering) is reliably detected.
16
+ *
17
+ * When the flag is set, this hook will emit a console error. This primarily
18
+ * helps catch the anti-pattern but may also flag valid nested structures.
19
+ *
20
+ * @param componentName - The name of the component calling this hook (for error messages)
21
+ */
22
+ export const useQuilttRenderGuard = (componentName: string) => {
23
+ const { isRenderingProvider } = useContext(QuilttProviderRender)
24
+ const hasWarnedRef = useRef(false)
25
+
26
+ useEffect(() => {
27
+ // Only run in development mode and warn once per component instance
28
+ // Support both Node.js (process.env) and Vite (import.meta.env) environments
29
+ const isProduction =
30
+ process.env.NODE_ENV === 'production' ||
31
+ (typeof import.meta !== 'undefined' && (import.meta as any).env?.PROD)
32
+
33
+ if (isProduction) return
34
+
35
+ if (isRenderingProvider && !hasWarnedRef.current) {
36
+ hasWarnedRef.current = true
37
+ console.error(
38
+ `[Quiltt] ⚠️ POTENTIAL ANTI-PATTERN: ${componentName} may be rendered in the same component as QuilttProvider.\n\n` +
39
+ `NOTE: This check uses a simple flag and may produce false positives for valid nested patterns.\n` +
40
+ `If ${componentName} is in a SEPARATE component from QuilttProvider, you can safely ignore this.\n\n` +
41
+ `The ANTI-PATTERN we're trying to catch:\n` +
42
+ ` • Rendering Provider and SDK components in the SAME function component\n` +
43
+ ` • This causes memory context issues because they run at the same React execution layer\n` +
44
+ ` • React cannot properly manage the context hierarchy in this case\n\n` +
45
+ `RECOMMENDED PATTERN:\n` +
46
+ ` • Move QuilttProvider to a parent component or layout\n` +
47
+ ` • Render ${componentName} in a child component\n\n` +
48
+ `Example:\n\n` +
49
+ ` // ✅ CORRECT: Provider in parent, SDK component in child (separate components)\n` +
50
+ ` function Providers({ children }) {\n` +
51
+ ` return (\n` +
52
+ ` <QuilttProvider token={token}>\n` +
53
+ ` {children}\n` +
54
+ ` </QuilttProvider>\n` +
55
+ ` )\n` +
56
+ ` }\n\n` +
57
+ ` function MyFeature() {\n` +
58
+ ` return <${componentName} connectorId="..." />\n` +
59
+ ` }\n\n` +
60
+ ` // ❌ ANTI-PATTERN: Both in SAME component\n` +
61
+ ` function MyFeature() {\n` +
62
+ ` return (\n` +
63
+ ` <QuilttProvider token={token}>\n` +
64
+ ` <${componentName} connectorId="..." />\n` +
65
+ ` </QuilttProvider>\n` +
66
+ ` )\n` +
67
+ ` }\n\n` +
68
+ `Learn more:\n` +
69
+ ` • https://github.com/quiltt/quiltt-js/tree/main/packages/react#provider-usage\n` +
70
+ ` • https://react.dev/reference/react/useContext#my-component-doesnt-see-the-value-from-my-provider`
71
+ )
72
+ }
73
+ }, [isRenderingProvider, componentName])
74
+ }
75
+
76
+ export default useQuilttRenderGuard
@@ -0,0 +1,128 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
4
+
5
+ import type { ErrorData, ResolvableData } from '@quiltt/core'
6
+ import { ConnectorsAPI } from '@quiltt/core'
7
+
8
+ import { version } from '../version'
9
+ import useSession from './useSession'
10
+
11
+ export type UseQuilttResolvable = (
12
+ connectorId: string,
13
+ onErrorCallback?: (msg: string) => void
14
+ ) => {
15
+ checkResolvable: (providerId: {
16
+ plaid?: string
17
+ mock?: string
18
+ mx?: string
19
+ finicity?: string
20
+ akoya?: string
21
+ }) => Promise<boolean | null>
22
+ isLoading: boolean
23
+ isResolvable: boolean | null
24
+ error: string | null
25
+ }
26
+
27
+ export const useQuilttResolvable: UseQuilttResolvable = (connectorId, onErrorCallback) => {
28
+ const agent = useMemo(() => {
29
+ // Try deprecated navigator.product first (still used in some RN versions)
30
+ if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
31
+ return `react-native-${version}`
32
+ }
33
+
34
+ // Detect React Native by its unique environment characteristics
35
+ const isReactNative = !!(
36
+ // Has window (unlike Node.js)
37
+ (
38
+ typeof window !== 'undefined' &&
39
+ // No document in window (unlike browsers)
40
+ typeof window.document === 'undefined' &&
41
+ // Has navigator (unlike Node.js)
42
+ typeof navigator !== 'undefined'
43
+ )
44
+ )
45
+
46
+ return isReactNative ? `react-native-${version}` : `react-${version}`
47
+ }, [])
48
+
49
+ const connectorsAPI = useMemo(() => new ConnectorsAPI(connectorId, agent), [connectorId, agent])
50
+ const [session] = useSession()
51
+
52
+ const [isLoading, setIsLoading] = useState(false)
53
+ const [isResolvable, setIsResolvable] = useState<boolean | null>(null)
54
+ const [error, setError] = useState<string | null>(null)
55
+
56
+ // Store callback in ref to maintain stable reference
57
+ const onErrorCallbackRef = useRef(onErrorCallback)
58
+ useEffect(() => {
59
+ onErrorCallbackRef.current = onErrorCallback
60
+ })
61
+
62
+ const handleError = useCallback((message: string) => {
63
+ const errorMessage = message || 'Unknown error occurred while checking resolvability'
64
+
65
+ setError(errorMessage)
66
+ console.error('Quiltt Connector Resolvable Error:', errorMessage)
67
+ if (onErrorCallbackRef.current) onErrorCallbackRef.current(errorMessage)
68
+ }, [])
69
+
70
+ const checkResolvable = useCallback(
71
+ async (providerId: {
72
+ plaid?: string
73
+ mock?: string
74
+ mx?: string
75
+ finicity?: string
76
+ akoya?: string
77
+ }): Promise<boolean | null> => {
78
+ if (!session?.token) {
79
+ handleError('Missing session token')
80
+ return null
81
+ }
82
+
83
+ if (!connectorId) {
84
+ handleError('Missing connector ID')
85
+ return null
86
+ }
87
+
88
+ const hasProviderId = Object.values(providerId).some((id) => !!id)
89
+ if (!hasProviderId) {
90
+ handleError('No provider ID specified')
91
+ return null
92
+ }
93
+
94
+ setIsLoading(true)
95
+ setError(null)
96
+
97
+ try {
98
+ const response = await connectorsAPI.checkResolvable(session.token, connectorId, providerId)
99
+
100
+ if (response.status === 200) {
101
+ const result = (response.data as ResolvableData).resolvable
102
+ setIsResolvable(result)
103
+ return result
104
+ }
105
+
106
+ handleError((response.data as ErrorData).message || 'Failed to check resolvability')
107
+ setIsResolvable(null)
108
+ return null
109
+ } catch (error: any) {
110
+ handleError(error?.message)
111
+ setIsResolvable(null)
112
+ return null
113
+ } finally {
114
+ setIsLoading(false)
115
+ }
116
+ },
117
+ [session?.token, connectorId, connectorsAPI, handleError]
118
+ )
119
+
120
+ return {
121
+ checkResolvable,
122
+ isLoading,
123
+ isResolvable,
124
+ error,
125
+ }
126
+ }
127
+
128
+ export default useQuilttResolvable
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useCallback } from 'react'
3
+ import { useCallback, useMemo } from 'react'
4
4
 
5
5
  import type { Maybe, QuilttJWT } from '@quiltt/core'
6
6
  import { AuthAPI } from '@quiltt/core'
@@ -28,7 +28,7 @@ export const useQuilttSession: UseQuilttSession = (environmentId) => {
28
28
  const { clientId } = useQuilttSettings()
29
29
  const [session, setSession] = useSession()
30
30
 
31
- const auth = new AuthAPI(clientId)
31
+ const auth = useMemo(() => new AuthAPI(clientId), [clientId])
32
32
  const importSession = useImportSession(auth, session, setSession, environmentId)
33
33
  const identifySession = useIdentifySession(auth, setSession)
34
34
  const authenticateSession = useAuthenticateSession(auth, setSession)
@@ -1,5 +1,7 @@
1
1
  import type { FC } from 'react'
2
2
 
3
+ import { QuilttProviderRender } from '@/contexts/QuilttProviderRender'
4
+
3
5
  import type { QuilttAuthProviderProps } from './QuilttAuthProvider'
4
6
  import { QuilttAuthProvider } from './QuilttAuthProvider'
5
7
  import type { QuilttSettingsProviderProps } from './QuilttSettingsProvider'
@@ -13,12 +15,19 @@ export const QuilttProvider: FC<QuilttProviderProps> = ({
13
15
  token,
14
16
  children,
15
17
  }) => {
18
+ // Set a context flag that SDK components can check to warn about potential anti-patterns.
19
+ // LIMITATION: This flags ALL descendants due to React context propagation, not just same-component usage.
20
+ // Will produce false positives for valid patterns like: <QuilttProvider><MyPage /></QuilttProvider>
21
+ // where MyPage renders SDK components (which is correct usage).
22
+ // The flag-based approach is simple but imprecise - a proper solution would require render stack tracking.
16
23
  return (
17
- <QuilttSettingsProvider clientId={clientId}>
18
- <QuilttAuthProvider token={token} graphqlClient={graphqlClient}>
19
- {children}
20
- </QuilttAuthProvider>
21
- </QuilttSettingsProvider>
24
+ <QuilttProviderRender.Provider value={{ isRenderingProvider: true }}>
25
+ <QuilttSettingsProvider clientId={clientId}>
26
+ <QuilttAuthProvider token={token} graphqlClient={graphqlClient}>
27
+ {children}
28
+ </QuilttAuthProvider>
29
+ </QuilttSettingsProvider>
30
+ </QuilttProviderRender.Provider>
22
31
  )
23
32
  }
24
33