@quiltt/react 4.4.0 → 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,18 @@
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
+
3
16
  ## 4.4.0
4
17
 
5
18
  ### Minor 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
@@ -117,6 +117,31 @@ declare const useQuilttConnector: (connectorId?: string, options?: ConnectorSDKC
117
117
  open: () => void;
118
118
  };
119
119
 
120
+ type UseQuilttInstitutions = (connectorId: string, onErrorCallback?: (msg: string) => void) => {
121
+ searchTerm: string;
122
+ searchResults: InstitutionsData;
123
+ isSearching: boolean;
124
+ setSearchTerm: (term: string) => void;
125
+ };
126
+ declare const useQuilttInstitutions: UseQuilttInstitutions;
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
+
120
145
  type UseQuilttResolvable = (connectorId: string, onErrorCallback?: (msg: string) => void) => {
121
146
  checkResolvable: (providerId: {
122
147
  plaid?: string;
@@ -131,14 +156,6 @@ type UseQuilttResolvable = (connectorId: string, onErrorCallback?: (msg: string)
131
156
  };
132
157
  declare const useQuilttResolvable: UseQuilttResolvable;
133
158
 
134
- type UseQuilttInstitutions = (connectorId: string, onErrorCallback?: (msg: string) => void) => {
135
- searchTerm: string;
136
- searchResults: InstitutionsData;
137
- isSearching: boolean;
138
- setSearchTerm: (term: string) => void;
139
- };
140
- declare const useQuilttInstitutions: UseQuilttInstitutions;
141
-
142
159
  type UseQuilttSession = (environmentId?: string) => {
143
160
  session: Maybe<QuilttJWT> | undefined;
144
161
  importSession: ImportSession;
@@ -196,5 +213,5 @@ declare const QuilttSettingsProvider: FC<QuilttSettingsProviderProps>;
196
213
  type QuilttProviderProps = QuilttSettingsProviderProps & QuilttAuthProviderProps;
197
214
  declare const QuilttProvider: FC<QuilttProviderProps>;
198
215
 
199
- export { QuilttAuthProvider, QuilttButton, QuilttContainer, QuilttProvider, QuilttSettingsProvider, useAuthenticateSession, useEventListener, useIdentifySession, useImportSession, useIsomorphicLayoutEffect, useQuilttClient, useQuilttConnector, useQuilttInstitutions, useQuilttResolvable, useQuilttSession, useQuilttSettings, useRevokeSession, useSession, useStorage };
216
+ export { QuilttAuthProvider, QuilttButton, QuilttContainer, QuilttProvider, QuilttSettingsProvider, useAuthenticateSession, useEventListener, useIdentifySession, useImportSession, useIsomorphicLayoutEffect, useQuilttClient, useQuilttConnector, useQuilttInstitutions, useQuilttRenderGuard, useQuilttResolvable, useQuilttSession, useQuilttSettings, useRevokeSession, useSession, useStorage };
200
217
  export type { AuthenticateSession, IdentifySession, ImportSession, QuilttAuthProviderProps, QuilttSettingsProviderProps, RevokeSession, SetSession, UseQuilttInstitutions, UseQuilttResolvable, UseQuilttSession };
package/dist/index.js CHANGED
@@ -1,18 +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-BiIbmt7I.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 useQuilttResolvable } from './useQuilttResolvable-12s-C2mjy5zs.js';
11
- export { u as useQuilttInstitutions } from './useQuilttInstitutions-12s-0_y37UHg.js';
12
- 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';
13
14
  export { u as useQuilttSettings } from './useQuilttSettings-12s--rCJoNHD.js';
14
15
  export { u as useSession } from './useSession-12s-7GOn4sUn.js';
15
16
  export { u as useStorage } from './useStorage-12s-DHcq3Kuh.js';
17
+ import { Q as QuilttProviderRender } from './QuilttProviderRender-12s-DtQtubjL.js';
16
18
  import { Q as QuilttSettingsProvider } from './QuilttSettingsProvider-12s-ZcmFmOiZ.js';
17
19
 
18
20
  /**
@@ -22,6 +24,8 @@ import { Q as QuilttSettingsProvider } from './QuilttSettingsProvider-12s-ZcmFmO
22
24
  * connector instance with the new connection details. If you need to force a
23
25
  * complete remount instead, set forceRemountOnConnectionChange to true.
24
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');
25
29
  // Keep track of previous connectionId for change detection
26
30
  const prevConnectionIdRef = useRef(connectionId);
27
31
  const prevCallbacksRef = useRef({
@@ -104,6 +108,8 @@ import { Q as QuilttSettingsProvider } from './QuilttSettingsProvider-12s-ZcmFmO
104
108
  * connector instance with the new connection details. If you need to force a
105
109
  * complete remount instead, set forceRemountOnConnectionChange to true.
106
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');
107
113
  // Keep track of previous connectionId for change detection
108
114
  const prevConnectionIdRef = useRef(connectionId);
109
115
  const prevCallbacksRef = useRef({
@@ -163,14 +169,24 @@ import { Q as QuilttSettingsProvider } from './QuilttSettingsProvider-12s-ZcmFmO
163
169
  };
164
170
 
165
171
  const QuilttProvider = ({ clientId, graphqlClient, token, children })=>{
166
- return /*#__PURE__*/ jsx(QuilttSettingsProvider, {
167
- clientId: clientId,
168
- children: /*#__PURE__*/ jsx(QuilttAuthProvider, {
169
- token: token,
170
- graphqlClient: graphqlClient,
171
- 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
+ })
172
188
  })
173
189
  });
174
190
  };
175
191
 
176
- 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.4.0";
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}`, {
@@ -2,7 +2,7 @@
2
2
  import { useMemo, useState, useRef, useEffect, useCallback } from 'react';
3
3
  import { ConnectorsAPI } from '@quiltt/core';
4
4
  import { useDebounce } from 'use-debounce';
5
- import { v as version } from './useQuilttConnector-12s-BiIbmt7I.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)=>{
@@ -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 };
@@ -1,7 +1,7 @@
1
1
  'use client';
2
- import { useMemo, useState, useCallback } from 'react';
2
+ import { useMemo, useState, useRef, useEffect, useCallback } from 'react';
3
3
  import { ConnectorsAPI } from '@quiltt/core';
4
- import { v as version } from './useQuilttConnector-12s-BiIbmt7I.js';
4
+ import { v as version } from './useQuilttConnector-12s-7jon0A8C.js';
5
5
  import { u as useSession } from './useSession-12s-7GOn4sUn.js';
6
6
 
7
7
  const useQuilttResolvable = (connectorId, onErrorCallback)=>{
@@ -25,17 +25,24 @@ const useQuilttResolvable = (connectorId, onErrorCallback)=>{
25
25
  const [isLoading, setIsLoading] = useState(false);
26
26
  const [isResolvable, setIsResolvable] = useState(null);
27
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
+ });
28
33
  const handleError = useCallback((message)=>{
29
34
  const errorMessage = message || 'Unknown error occurred while checking resolvability';
30
35
  setError(errorMessage);
31
36
  console.error('Quiltt Connector Resolvable Error:', errorMessage);
32
- if (onErrorCallback) onErrorCallback(errorMessage);
33
- }, [
34
- onErrorCallback
35
- ]);
37
+ if (onErrorCallbackRef.current) onErrorCallbackRef.current(errorMessage);
38
+ }, []);
36
39
  const checkResolvable = useCallback(async (providerId)=>{
37
- if (!session?.token || !connectorId) {
38
- handleError('Missing session token or connector ID');
40
+ if (!session?.token) {
41
+ handleError('Missing session token');
42
+ return null;
43
+ }
44
+ if (!connectorId) {
45
+ handleError('Missing connector ID');
39
46
  return null;
40
47
  }
41
48
  const hasProviderId = Object.values(providerId).some((id)=>!!id);
@@ -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.4.0",
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.4.0"
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>({})
@@ -2,8 +2,9 @@ export * from './helpers'
2
2
  export * from './session'
3
3
  export * from './useQuilttClient'
4
4
  export * from './useQuilttConnector'
5
- export * from './useQuilttResolvable'
6
5
  export * from './useQuilttInstitutions'
6
+ export * from './useQuilttRenderGuard'
7
+ export * from './useQuilttResolvable'
7
8
  export * from './useQuilttSession'
8
9
  export * from './useQuilttSettings'
9
10
  export * from './useSession'
@@ -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
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useCallback, useMemo, useState } from 'react'
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
4
4
 
5
5
  import type { ErrorData, ResolvableData } from '@quiltt/core'
6
6
  import { ConnectorsAPI } from '@quiltt/core'
@@ -10,7 +10,7 @@ import useSession from './useSession'
10
10
 
11
11
  export type UseQuilttResolvable = (
12
12
  connectorId: string,
13
- onErrorCallback?: (msg: string) => void,
13
+ onErrorCallback?: (msg: string) => void
14
14
  ) => {
15
15
  checkResolvable: (providerId: {
16
16
  plaid?: string
@@ -24,10 +24,7 @@ export type UseQuilttResolvable = (
24
24
  error: string | null
25
25
  }
26
26
 
27
- export const useQuilttResolvable: UseQuilttResolvable = (
28
- connectorId,
29
- onErrorCallback,
30
- ) => {
27
+ export const useQuilttResolvable: UseQuilttResolvable = (connectorId, onErrorCallback) => {
31
28
  const agent = useMemo(() => {
32
29
  // Try deprecated navigator.product first (still used in some RN versions)
33
30
  if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
@@ -56,16 +53,19 @@ export const useQuilttResolvable: UseQuilttResolvable = (
56
53
  const [isResolvable, setIsResolvable] = useState<boolean | null>(null)
57
54
  const [error, setError] = useState<string | null>(null)
58
55
 
59
- const handleError = useCallback(
60
- (message: string) => {
61
- const errorMessage = message || 'Unknown error occurred while checking resolvability'
56
+ // Store callback in ref to maintain stable reference
57
+ const onErrorCallbackRef = useRef(onErrorCallback)
58
+ useEffect(() => {
59
+ onErrorCallbackRef.current = onErrorCallback
60
+ })
62
61
 
63
- setError(errorMessage)
64
- console.error('Quiltt Connector Resolvable Error:', errorMessage)
65
- if (onErrorCallback) onErrorCallback(errorMessage)
66
- },
67
- [onErrorCallback],
68
- )
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
69
 
70
70
  const checkResolvable = useCallback(
71
71
  async (providerId: {
@@ -75,8 +75,13 @@ export const useQuilttResolvable: UseQuilttResolvable = (
75
75
  finicity?: string
76
76
  akoya?: string
77
77
  }): Promise<boolean | null> => {
78
- if (!session?.token || !connectorId) {
79
- handleError('Missing session token or connector ID')
78
+ if (!session?.token) {
79
+ handleError('Missing session token')
80
+ return null
81
+ }
82
+
83
+ if (!connectorId) {
84
+ handleError('Missing connector ID')
80
85
  return null
81
86
  }
82
87
 
@@ -90,11 +95,7 @@ export const useQuilttResolvable: UseQuilttResolvable = (
90
95
  setError(null)
91
96
 
92
97
  try {
93
- const response = await connectorsAPI.checkResolvable(
94
- session.token,
95
- connectorId,
96
- providerId,
97
- )
98
+ const response = await connectorsAPI.checkResolvable(session.token, connectorId, providerId)
98
99
 
99
100
  if (response.status === 200) {
100
101
  const result = (response.data as ResolvableData).resolvable
@@ -113,7 +114,7 @@ export const useQuilttResolvable: UseQuilttResolvable = (
113
114
  setIsLoading(false)
114
115
  }
115
116
  },
116
- [session?.token, connectorId, connectorsAPI, handleError],
117
+ [session?.token, connectorId, connectorsAPI, handleError]
117
118
  )
118
119
 
119
120
  return {
@@ -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