@quiltt/react-native 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,61 @@
1
1
  # @quiltt/react-native
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
+ - @quiltt/react@4.5.0
16
+
17
+ ## 4.4.0
18
+
19
+ ### Minor Changes
20
+
21
+ - [#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.
22
+
23
+ ## New APIs
24
+
25
+ ### `useQuilttResolvable` Hook
26
+
27
+ Check if external provider institution IDs (e.g., Plaid) can be migrated to your connector.
28
+
29
+ ```typescript
30
+ import { useQuilttResolvable } from "@quiltt/react";
31
+ import { useEffect } from "react";
32
+
33
+ function ResolvableConnector({ plaidInstitutionId, children }) {
34
+ const { checkResolvable, isResolvable, isLoading } =
35
+ useQuilttResolvable("my-connector-id");
36
+
37
+ useEffect(() => {
38
+ checkResolvable({ plaid: plaidInstitutionId });
39
+ }, [plaidInstitutionId]);
40
+
41
+ if (isLoading) return <div>Checking...</div>;
42
+ if (!isResolvable) return null;
43
+
44
+ return <>{children}</>;
45
+ }
46
+
47
+ // Usage
48
+ <ResolvableConnector plaidInstitutionId="ins_3">
49
+ <QuilttButton connectorId="my-connector-id" />
50
+ </ResolvableConnector>;
51
+ ```
52
+
53
+ ### Patch Changes
54
+
55
+ - Updated dependencies [[`0af4e66`](https://github.com/quiltt/quiltt-js/commit/0af4e6622d1542e0c0c02ac7e897e3e4f9219cbd)]:
56
+ - @quiltt/react@4.4.0
57
+ - @quiltt/core@4.4.0
58
+
3
59
  ## 4.3.3
4
60
 
5
61
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export * from '@quiltt/core';
2
2
  import { ConnectorSDKCallbacks } from '@quiltt/react';
3
3
  export { QuilttAuthProvider, QuilttProvider, QuilttSettingsProvider, useQuilttClient, useQuilttConnector, useQuilttSession, useQuilttSettings, useSession, useStorage } from '@quiltt/react';
4
- import * as react_jsx_runtime from 'react/jsx-runtime';
4
+ import * as react from 'react';
5
5
  import { URL } from 'react-native-url-polyfill';
6
6
 
7
7
  type PreFlightCheck = {
@@ -13,17 +13,16 @@ declare const checkConnectorUrl: (connectorUrl: string, retryCount?: number) =>
13
13
  * Handle opening OAuth URLs with proper encoding detection and normalization
14
14
  */
15
15
  declare const handleOAuthUrl: (oauthUrl: URL | string | null | undefined) => void;
16
- type QuilttConnectorProps = {
16
+ type QuilttConnectorHandle = {
17
+ handleOAuthCallback: (url: string) => void;
18
+ };
19
+ declare const QuilttConnector: react.ForwardRefExoticComponent<{
17
20
  connectorId: string;
18
21
  connectionId?: string;
19
22
  institution?: string;
20
23
  oauthRedirectUrl: string;
21
24
  testId?: string;
22
- } & ConnectorSDKCallbacks;
23
- declare const QuilttConnector: {
24
- ({ connectorId, connectionId, institution, oauthRedirectUrl, onEvent, onLoad, onExit, onExitSuccess, onExitAbort, onExitError, testId, }: QuilttConnectorProps): react_jsx_runtime.JSX.Element;
25
- displayName: string;
26
- };
25
+ } & ConnectorSDKCallbacks & react.RefAttributes<QuilttConnectorHandle>>;
27
26
 
28
27
  export { QuilttConnector, checkConnectorUrl, handleOAuthUrl };
29
- export type { PreFlightCheck };
28
+ export type { PreFlightCheck, QuilttConnectorHandle };
package/dist/index.js CHANGED
@@ -3,13 +3,13 @@ export * from '@quiltt/core';
3
3
  import { useQuilttSession, ConnectorSDKEventType } from '@quiltt/react';
4
4
  export { QuilttAuthProvider, QuilttProvider, QuilttSettingsProvider, useQuilttClient, useQuilttConnector, useQuilttSession, useQuilttSettings, useSession, useStorage } from '@quiltt/react';
5
5
  import { jsx, jsxs } from 'react/jsx-runtime';
6
- import { useRef, useState, useCallback, useMemo, useEffect } from 'react';
6
+ import { forwardRef, useRef, useState, useCallback, useMemo, useEffect, useImperativeHandle } from 'react';
7
7
  import { StyleSheet, StatusBar, Platform, SafeAreaView, View, Text, Pressable, ActivityIndicator, Linking } from 'react-native';
8
8
  import { URL } from 'react-native-url-polyfill';
9
9
  import { WebView } from 'react-native-webview';
10
10
  import { generateStackTrace, makeBacktrace, getCauses } from '@honeybadger-io/core/build/src/util';
11
11
 
12
- var version = "4.3.3";
12
+ var version = "4.5.0";
13
13
 
14
14
  // Custom Error Reporter to avoid hooking into or colliding with a client's Honeybadger singleton
15
15
  const notifier = {
@@ -115,12 +115,12 @@ const getErrorMessage = (responseStatus, error)=>{
115
115
  if (!str) return str;
116
116
  // If it's already encoded, return as is
117
117
  if (isEncoded(str)) {
118
- console.log('URL already encoded, skipping encoding:', str);
118
+ console.log('URL already encoded, skipping encoding');
119
119
  return str;
120
120
  }
121
121
  // Otherwise, encode it
122
122
  const encoded = encodeURIComponent(str);
123
- console.log('URL encoded from:', str, 'to:', encoded);
123
+ console.log('URL encoded');
124
124
  return encoded;
125
125
  };
126
126
  /**
@@ -349,7 +349,7 @@ const checkConnectorUrl = async (connectorUrl, retryCount = 0)=>{
349
349
  }
350
350
  }
351
351
  };
352
- const QuilttConnector = ({ connectorId, connectionId, institution, oauthRedirectUrl, onEvent, onLoad, onExit, onExitSuccess, onExitAbort, onExitError, testId })=>{
352
+ const QuilttConnector = /*#__PURE__*/ forwardRef(({ connectorId, connectionId, institution, oauthRedirectUrl, onEvent, onLoad, onExit, onExitSuccess, onExitAbort, onExitError, testId }, ref)=>{
353
353
  const webViewRef = useRef(null);
354
354
  const { session } = useQuilttSession();
355
355
  const [preFlightCheck, setPreFlightCheck] = useState({
@@ -535,6 +535,44 @@ const QuilttConnector = ({ connectorId, connectionId, institution, oauthRedirect
535
535
  isQuilttEvent,
536
536
  shouldRender
537
537
  ]);
538
+ // Expose method to handle OAuth callbacks from parent component
539
+ useImperativeHandle(ref, ()=>({
540
+ handleOAuthCallback: (callbackUrl)=>{
541
+ try {
542
+ console.log('Handling OAuth callback:', callbackUrl);
543
+ const url = new URL(callbackUrl);
544
+ // Extract OAuth callback parameters
545
+ const oauthParams = {};
546
+ url.searchParams.forEach((value, key)=>{
547
+ oauthParams[key] = value;
548
+ });
549
+ // Send OAuth callback data to the connector via postMessage
550
+ // This preserves the connector's state and allows events to fire properly
551
+ const message = {
552
+ source: 'quiltt',
553
+ type: 'OAuthCallback',
554
+ data: {
555
+ url: callbackUrl,
556
+ params: oauthParams
557
+ }
558
+ };
559
+ const script = `
560
+ (function() {
561
+ try {
562
+ window.postMessage(${JSON.stringify(message)});
563
+ console.log('OAuth callback message sent to connector');
564
+ } catch (e) {
565
+ console.error('Failed to send OAuth callback message:', e);
566
+ }
567
+ })();
568
+ true;
569
+ `;
570
+ webViewRef.current?.injectJavaScript(script);
571
+ } catch (error) {
572
+ console.error('Error handling OAuth callback:', error);
573
+ }
574
+ }
575
+ }), []);
538
576
  if (!preFlightCheck.checked) {
539
577
  return /*#__PURE__*/ jsx(LoadingScreen, {
540
578
  testId: "loading-screen"
@@ -554,42 +592,42 @@ const QuilttConnector = ({ connectorId, connectionId, institution, oauthRedirect
554
592
  children: /*#__PURE__*/ jsx(WebView, {
555
593
  ref: webViewRef,
556
594
  // Plaid keeps sending window.location = 'about:srcdoc' and causes some noise in RN
557
- style: styles.webview,
595
+ domStorageEnabled: true,
596
+ javaScriptEnabled: true,
597
+ onLoadEnd: onLoadEnd,
598
+ onShouldStartLoadWithRequest: requestHandler,
558
599
  originWhitelist: [
559
600
  '*'
560
601
  ],
602
+ scrollEnabled: true,
603
+ showsHorizontalScrollIndicator: false,
604
+ showsVerticalScrollIndicator: false,
561
605
  source: {
562
606
  uri: connectorUrl
563
607
  },
564
- onShouldStartLoadWithRequest: requestHandler,
565
- onLoadEnd: onLoadEnd,
566
- javaScriptEnabled: true,
567
- domStorageEnabled: true,
568
- webviewDebuggingEnabled: true,
569
- bounces: false,
570
- scrollEnabled: true,
571
- automaticallyAdjustContentInsets: false,
572
- contentInsetAdjustmentBehavior: "never",
573
- showsVerticalScrollIndicator: false,
574
- showsHorizontalScrollIndicator: false,
608
+ style: styles.webview,
575
609
  testID: "webview",
610
+ webviewDebuggingEnabled: true,
576
611
  ...Platform.OS === 'ios' ? {
612
+ allowsBackForwardNavigationGestures: false,
613
+ allowsInlineMediaPlayback: true,
614
+ automaticallyAdjustContentInsets: false,
615
+ bounces: false,
616
+ contentInsetAdjustmentBehavior: 'never',
617
+ dataDetectorTypes: 'none',
577
618
  decelerationRate: 'normal',
578
619
  keyboardDisplayRequiresUserAction: false,
579
- dataDetectorTypes: 'none',
580
- allowsInlineMediaPlayback: true,
581
- allowsBackForwardNavigationGestures: false,
582
- startInLoadingState: true,
583
620
  scrollEventThrottle: 16,
584
- overScrollMode: 'never'
621
+ startInLoadingState: true
585
622
  } : {
586
623
  androidLayerType: 'hardware',
587
624
  cacheEnabled: true,
588
- cacheMode: 'LOAD_CACHE_ELSE_NETWORK'
625
+ cacheMode: 'LOAD_CACHE_ELSE_NETWORK',
626
+ overScrollMode: 'never'
589
627
  }
590
628
  })
591
629
  });
592
- };
630
+ });
593
631
  // Add styles for the WebView container
594
632
  const styles = StyleSheet.create({
595
633
  webviewContainer: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quiltt/react-native",
3
- "version": "4.3.3",
3
+ "version": "4.5.0",
4
4
  "description": "React Native Components for Quiltt Connector",
5
5
  "homepage": "https://github.com/quiltt/quiltt-js/tree/main/packages/react-native#readme",
6
6
  "repository": {
@@ -30,8 +30,8 @@
30
30
  "dependencies": {
31
31
  "@honeybadger-io/core": "6.6.0",
32
32
  "lodash.debounce": "4.0.8",
33
- "@quiltt/core": "4.3.3",
34
- "@quiltt/react": "4.3.3"
33
+ "@quiltt/core": "4.5.0",
34
+ "@quiltt/react": "4.5.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@biomejs/biome": "2.2.4",
@@ -1,4 +1,12 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
1
+ import {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from 'react'
2
10
  import { Linking, Platform, StyleSheet } from 'react-native'
3
11
 
4
12
  import type { ConnectorSDKCallbackMetadata, ConnectorSDKCallbacks } from '@quiltt/react'
@@ -130,6 +138,10 @@ export const handleOAuthUrl = (oauthUrl: URL | string | null | undefined) => {
130
138
  }
131
139
  }
132
140
 
141
+ export type QuilttConnectorHandle = {
142
+ handleOAuthCallback: (url: string) => void
143
+ }
144
+
133
145
  type QuilttConnectorProps = {
134
146
  connectorId: string
135
147
  connectionId?: string
@@ -138,25 +150,29 @@ type QuilttConnectorProps = {
138
150
  testId?: string
139
151
  } & ConnectorSDKCallbacks
140
152
 
141
- const QuilttConnector = ({
142
- connectorId,
143
- connectionId,
144
- institution,
145
- oauthRedirectUrl,
146
- onEvent,
147
- onLoad,
148
- onExit,
149
- onExitSuccess,
150
- onExitAbort,
151
- onExitError,
152
- testId,
153
- }: QuilttConnectorProps) => {
154
- const webViewRef = useRef<WebView>(null)
155
- const { session } = useQuilttSession()
156
- const [preFlightCheck, setPreFlightCheck] = useState<PreFlightCheck>({ checked: false })
157
-
158
- // Script to disable scrolling on header
159
- const disableHeaderScrollScript = `
153
+ const QuilttConnector = forwardRef<QuilttConnectorHandle, QuilttConnectorProps>(
154
+ (
155
+ {
156
+ connectorId,
157
+ connectionId,
158
+ institution,
159
+ oauthRedirectUrl,
160
+ onEvent,
161
+ onLoad,
162
+ onExit,
163
+ onExitSuccess,
164
+ onExitAbort,
165
+ onExitError,
166
+ testId,
167
+ },
168
+ ref
169
+ ) => {
170
+ const webViewRef = useRef<WebView>(null)
171
+ const { session } = useQuilttSession()
172
+ const [preFlightCheck, setPreFlightCheck] = useState<PreFlightCheck>({ checked: false })
173
+
174
+ // Script to disable scrolling on header
175
+ const disableHeaderScrollScript = `
160
176
  (function() {
161
177
  const header = document.querySelector('header');
162
178
  if (header) {
@@ -169,48 +185,48 @@ const QuilttConnector = ({
169
185
  })();
170
186
  `
171
187
 
172
- const onLoadEnd = useCallback(() => {
173
- if (Platform.OS === 'ios') {
174
- webViewRef.current?.injectJavaScript(disableHeaderScrollScript)
175
- }
176
- }, [])
177
-
178
- // Ensure oauthRedirectUrl is encoded properly - only once
179
- const safeOAuthRedirectUrl = useMemo(() => {
180
- return smartEncodeURIComponent(oauthRedirectUrl)
181
- }, [oauthRedirectUrl])
182
-
183
- const connectorUrl = useMemo(() => {
184
- const url = new URL(`https://${connectorId}.quiltt.app`)
185
-
186
- // For normal parameters, just append them directly
187
- url.searchParams.append('mode', 'webview')
188
- url.searchParams.append('agent', `react-native-${version}`)
189
-
190
- // For the oauth_redirect_url, we need to be careful
191
- // If it's already encoded, we need to decode it once to prevent
192
- // the automatic encoding that happens with searchParams.append
193
- if (isEncoded(safeOAuthRedirectUrl)) {
194
- const decodedOnce = decodeURIComponent(safeOAuthRedirectUrl)
195
- url.searchParams.append('oauth_redirect_url', decodedOnce)
196
- } else {
197
- url.searchParams.append('oauth_redirect_url', safeOAuthRedirectUrl)
198
- }
188
+ const onLoadEnd = useCallback(() => {
189
+ if (Platform.OS === 'ios') {
190
+ webViewRef.current?.injectJavaScript(disableHeaderScrollScript)
191
+ }
192
+ }, [])
193
+
194
+ // Ensure oauthRedirectUrl is encoded properly - only once
195
+ const safeOAuthRedirectUrl = useMemo(() => {
196
+ return smartEncodeURIComponent(oauthRedirectUrl)
197
+ }, [oauthRedirectUrl])
198
+
199
+ const connectorUrl = useMemo(() => {
200
+ const url = new URL(`https://${connectorId}.quiltt.app`)
201
+
202
+ // For normal parameters, just append them directly
203
+ url.searchParams.append('mode', 'webview')
204
+ url.searchParams.append('agent', `react-native-${version}`)
205
+
206
+ // For the oauth_redirect_url, we need to be careful
207
+ // If it's already encoded, we need to decode it once to prevent
208
+ // the automatic encoding that happens with searchParams.append
209
+ if (isEncoded(safeOAuthRedirectUrl)) {
210
+ const decodedOnce = decodeURIComponent(safeOAuthRedirectUrl)
211
+ url.searchParams.append('oauth_redirect_url', decodedOnce)
212
+ } else {
213
+ url.searchParams.append('oauth_redirect_url', safeOAuthRedirectUrl)
214
+ }
199
215
 
200
- return url.toString()
201
- }, [connectorId, safeOAuthRedirectUrl])
216
+ return url.toString()
217
+ }, [connectorId, safeOAuthRedirectUrl])
202
218
 
203
- useEffect(() => {
204
- if (preFlightCheck.checked) return
205
- const fetchDataAndSetState = async () => {
206
- const connectorUrlStatus = await checkConnectorUrl(connectorUrl)
207
- setPreFlightCheck(connectorUrlStatus)
208
- }
209
- fetchDataAndSetState()
210
- }, [connectorUrl, preFlightCheck])
219
+ useEffect(() => {
220
+ if (preFlightCheck.checked) return
221
+ const fetchDataAndSetState = async () => {
222
+ const connectorUrlStatus = await checkConnectorUrl(connectorUrl)
223
+ setPreFlightCheck(connectorUrlStatus)
224
+ }
225
+ fetchDataAndSetState()
226
+ }, [connectorUrl, preFlightCheck])
211
227
 
212
- const initInjectedJavaScript = useCallback(() => {
213
- const script = `\
228
+ const initInjectedJavaScript = useCallback(() => {
229
+ const script = `\
214
230
  const options = {\
215
231
  source: 'quiltt',\
216
232
  type: 'Options',\
@@ -227,174 +243,222 @@ const QuilttConnector = ({
227
243
  }, {});\
228
244
  window.postMessage(compactedOptions);\
229
245
  `
230
- webViewRef.current?.injectJavaScript(script)
231
- }, [connectionId, connectorId, institution, session?.token])
232
-
233
- const isQuilttEvent = useCallback((url: URL) => url.protocol === 'quilttconnector:', [])
234
-
235
- const shouldRender = useCallback((url: URL) => !isQuilttEvent(url), [isQuilttEvent])
236
-
237
- const clearLocalStorage = useCallback(() => {
238
- const script = 'localStorage.clear();'
239
- webViewRef.current?.injectJavaScript(script)
240
- }, [])
241
-
242
- const handleQuilttEvent = useCallback(
243
- (url: URL) => {
244
- url.searchParams.delete('source')
245
- url.searchParams.append('connectorId', connectorId)
246
- const metadata = parseMetadata(url, connectorId)
247
-
248
- requestAnimationFrame(() => {
249
- const eventType = url.host
250
- switch (eventType) {
251
- case 'Load':
252
- console.log('Event: Load')
253
- initInjectedJavaScript()
254
- onEvent?.(ConnectorSDKEventType.Load, metadata)
255
- onLoad?.(metadata)
256
- break
257
- case 'ExitAbort':
258
- console.log('Event: ExitAbort')
259
- clearLocalStorage()
260
- onEvent?.(ConnectorSDKEventType.ExitAbort, metadata)
261
- onExit?.(ConnectorSDKEventType.ExitAbort, metadata)
262
- onExitAbort?.(metadata)
263
- break
264
- case 'ExitError':
265
- console.log('Event: ExitError')
266
- clearLocalStorage()
267
- onEvent?.(ConnectorSDKEventType.ExitError, metadata)
268
- onExit?.(ConnectorSDKEventType.ExitError, metadata)
269
- onExitError?.(metadata)
270
- break
271
- case 'ExitSuccess':
272
- console.log('Event: ExitSuccess')
273
- clearLocalStorage()
274
- onEvent?.(ConnectorSDKEventType.ExitSuccess, metadata)
275
- onExit?.(ConnectorSDKEventType.ExitSuccess, metadata)
276
- onExitSuccess?.(metadata)
277
- break
278
- case 'Authenticate':
279
- console.log('Event: Authenticate')
280
- // TODO: handle Authenticate
281
- break
282
- case 'Navigate': {
283
- console.log('Event: Navigate')
284
- const navigateUrl = url.searchParams.get('url')
285
-
286
- if (navigateUrl) {
287
- if (isEncoded(navigateUrl)) {
288
- try {
289
- const decodedUrl = decodeURIComponent(navigateUrl)
290
- handleOAuthUrl(decodedUrl)
291
- } catch (_error) {
292
- console.error('Navigate URL decoding failed, using original')
246
+ webViewRef.current?.injectJavaScript(script)
247
+ }, [connectionId, connectorId, institution, session?.token])
248
+
249
+ const isQuilttEvent = useCallback((url: URL) => url.protocol === 'quilttconnector:', [])
250
+
251
+ const shouldRender = useCallback((url: URL) => !isQuilttEvent(url), [isQuilttEvent])
252
+
253
+ const clearLocalStorage = useCallback(() => {
254
+ const script = 'localStorage.clear();'
255
+ webViewRef.current?.injectJavaScript(script)
256
+ }, [])
257
+
258
+ const handleQuilttEvent = useCallback(
259
+ (url: URL) => {
260
+ url.searchParams.delete('source')
261
+ url.searchParams.append('connectorId', connectorId)
262
+ const metadata = parseMetadata(url, connectorId)
263
+
264
+ requestAnimationFrame(() => {
265
+ const eventType = url.host
266
+ switch (eventType) {
267
+ case 'Load':
268
+ console.log('Event: Load')
269
+ initInjectedJavaScript()
270
+ onEvent?.(ConnectorSDKEventType.Load, metadata)
271
+ onLoad?.(metadata)
272
+ break
273
+ case 'ExitAbort':
274
+ console.log('Event: ExitAbort')
275
+ clearLocalStorage()
276
+ onEvent?.(ConnectorSDKEventType.ExitAbort, metadata)
277
+ onExit?.(ConnectorSDKEventType.ExitAbort, metadata)
278
+ onExitAbort?.(metadata)
279
+ break
280
+ case 'ExitError':
281
+ console.log('Event: ExitError')
282
+ clearLocalStorage()
283
+ onEvent?.(ConnectorSDKEventType.ExitError, metadata)
284
+ onExit?.(ConnectorSDKEventType.ExitError, metadata)
285
+ onExitError?.(metadata)
286
+ break
287
+ case 'ExitSuccess':
288
+ console.log('Event: ExitSuccess')
289
+ clearLocalStorage()
290
+ onEvent?.(ConnectorSDKEventType.ExitSuccess, metadata)
291
+ onExit?.(ConnectorSDKEventType.ExitSuccess, metadata)
292
+ onExitSuccess?.(metadata)
293
+ break
294
+ case 'Authenticate':
295
+ console.log('Event: Authenticate')
296
+ // TODO: handle Authenticate
297
+ break
298
+ case 'Navigate': {
299
+ console.log('Event: Navigate')
300
+ const navigateUrl = url.searchParams.get('url')
301
+
302
+ if (navigateUrl) {
303
+ if (isEncoded(navigateUrl)) {
304
+ try {
305
+ const decodedUrl = decodeURIComponent(navigateUrl)
306
+ handleOAuthUrl(decodedUrl)
307
+ } catch (_error) {
308
+ console.error('Navigate URL decoding failed, using original')
309
+ handleOAuthUrl(navigateUrl)
310
+ }
311
+ } else {
293
312
  handleOAuthUrl(navigateUrl)
294
313
  }
295
314
  } else {
296
- handleOAuthUrl(navigateUrl)
315
+ console.error('Navigate URL missing from request')
297
316
  }
298
- } else {
299
- console.error('Navigate URL missing from request')
317
+ break
300
318
  }
301
- break
319
+ // NOTE: The `OauthRequested` is deprecated and should not be used
320
+ default:
321
+ console.log(`Unhandled event: ${eventType}`)
322
+ break
302
323
  }
303
- // NOTE: The `OauthRequested` is deprecated and should not be used
304
- default:
305
- console.log(`Unhandled event: ${eventType}`)
306
- break
324
+ })
325
+ },
326
+ [
327
+ clearLocalStorage,
328
+ connectorId,
329
+ initInjectedJavaScript,
330
+ onEvent,
331
+ onExit,
332
+ onExitAbort,
333
+ onExitError,
334
+ onExitSuccess,
335
+ onLoad,
336
+ ]
337
+ )
338
+
339
+ const requestHandler = useCallback(
340
+ (request: ShouldStartLoadRequest) => {
341
+ const url = new URL(request.url)
342
+
343
+ if (isQuilttEvent(url)) {
344
+ handleQuilttEvent(url)
345
+ return false
307
346
  }
308
- })
309
- },
310
- [
311
- clearLocalStorage,
312
- connectorId,
313
- initInjectedJavaScript,
314
- onEvent,
315
- onExit,
316
- onExitAbort,
317
- onExitError,
318
- onExitSuccess,
319
- onLoad,
320
- ]
321
- )
322
347
 
323
- const requestHandler = useCallback(
324
- (request: ShouldStartLoadRequest) => {
325
- const url = new URL(request.url)
348
+ if (shouldRender(url)) {
349
+ return true
350
+ }
326
351
 
327
- if (isQuilttEvent(url)) {
328
- handleQuilttEvent(url)
352
+ // Plaid set oauth url by doing window.location.href = url
353
+ // So we use `handleOAuthUrl` as a catch all and assume all url got to this step is Plaid OAuth url
354
+ handleOAuthUrl(url)
329
355
  return false
330
- }
356
+ },
357
+ [handleQuilttEvent, isQuilttEvent, shouldRender]
358
+ )
331
359
 
332
- if (shouldRender(url)) {
333
- return true
334
- }
360
+ // Expose method to handle OAuth callbacks from parent component
361
+ useImperativeHandle(
362
+ ref,
363
+ () => ({
364
+ handleOAuthCallback: (callbackUrl: string) => {
365
+ try {
366
+ console.log('Handling OAuth callback:', callbackUrl)
367
+ const url = new URL(callbackUrl)
368
+
369
+ // Extract OAuth callback parameters
370
+ const oauthParams: Record<string, string> = {}
371
+ url.searchParams.forEach((value, key) => {
372
+ oauthParams[key] = value
373
+ })
374
+
375
+ // Send OAuth callback data to the connector via postMessage
376
+ // This preserves the connector's state and allows events to fire properly
377
+ const message = {
378
+ source: 'quiltt',
379
+ type: 'OAuthCallback',
380
+ data: {
381
+ url: callbackUrl,
382
+ params: oauthParams,
383
+ },
384
+ }
335
385
 
336
- // Plaid set oauth url by doing window.location.href = url
337
- // So we use `handleOAuthUrl` as a catch all and assume all url got to this step is Plaid OAuth url
338
- handleOAuthUrl(url)
339
- return false
340
- },
341
- [handleQuilttEvent, isQuilttEvent, shouldRender]
342
- )
386
+ const script = `
387
+ (function() {
388
+ try {
389
+ window.postMessage(${JSON.stringify(message)});
390
+ console.log('OAuth callback message sent to connector');
391
+ } catch (e) {
392
+ console.error('Failed to send OAuth callback message:', e);
393
+ }
394
+ })();
395
+ true;
396
+ `
343
397
 
344
- if (!preFlightCheck.checked) {
345
- return <LoadingScreen testId="loading-screen" />
346
- }
398
+ webViewRef.current?.injectJavaScript(script)
399
+ } catch (error) {
400
+ console.error('Error handling OAuth callback:', error)
401
+ }
402
+ },
403
+ }),
404
+ []
405
+ )
406
+
407
+ if (!preFlightCheck.checked) {
408
+ return <LoadingScreen testId="loading-screen" />
409
+ }
410
+
411
+ if (preFlightCheck.error) {
412
+ return (
413
+ <ErrorScreen
414
+ testId="error-screen"
415
+ error={preFlightCheck.error}
416
+ cta={() => onExitError?.({ connectorId })}
417
+ />
418
+ )
419
+ }
347
420
 
348
- if (preFlightCheck.error) {
349
421
  return (
350
- <ErrorScreen
351
- testId="error-screen"
352
- error={preFlightCheck.error}
353
- cta={() => onExitError?.({ connectorId })}
354
- />
422
+ <AndroidSafeAreaView testId={testId}>
423
+ <WebView
424
+ ref={webViewRef}
425
+ // Plaid keeps sending window.location = 'about:srcdoc' and causes some noise in RN
426
+ domStorageEnabled // To enable localStorage in Android webview
427
+ javaScriptEnabled
428
+ onLoadEnd={onLoadEnd}
429
+ onShouldStartLoadWithRequest={requestHandler}
430
+ originWhitelist={['*']}
431
+ scrollEnabled={true} // Enables scrolling within the WebView
432
+ showsHorizontalScrollIndicator={false}
433
+ showsVerticalScrollIndicator={false}
434
+ source={{ uri: connectorUrl }}
435
+ style={styles.webview}
436
+ testID="webview"
437
+ webviewDebuggingEnabled
438
+ {...(Platform.OS === 'ios'
439
+ ? {
440
+ allowsBackForwardNavigationGestures: false, // Disables swipe to go back/forward
441
+ allowsInlineMediaPlayback: true, // Allows videos to play inline
442
+ automaticallyAdjustContentInsets: false, // Disables automatic padding adjustments
443
+ bounces: false, // Controls the bouncing effect when scrolling past content boundaries
444
+ contentInsetAdjustmentBehavior: 'never', // Controls how safe area insets modify content
445
+ dataDetectorTypes: 'none', // Disables automatic data detection (phone numbers, links, etc.)
446
+ decelerationRate: 'normal', // Controls scroll deceleration speed
447
+ keyboardDisplayRequiresUserAction: false, // Allows programmatic keyboard display
448
+ scrollEventThrottle: 16, // Optimize scroll performance (throttle scroll events)
449
+ startInLoadingState: true, // Shows loading indicator on first load
450
+ }
451
+ : {
452
+ androidLayerType: 'hardware', // Use hardware acceleration for rendering
453
+ cacheEnabled: true, // Enable caching
454
+ cacheMode: 'LOAD_CACHE_ELSE_NETWORK', // Load from cache when available
455
+ overScrollMode: 'never', // Disable overscroll effect
456
+ })}
457
+ />
458
+ </AndroidSafeAreaView>
355
459
  )
356
460
  }
357
-
358
- return (
359
- <AndroidSafeAreaView testId={testId}>
360
- <WebView
361
- ref={webViewRef}
362
- // Plaid keeps sending window.location = 'about:srcdoc' and causes some noise in RN
363
- style={styles.webview}
364
- originWhitelist={['*']}
365
- source={{ uri: connectorUrl }}
366
- onShouldStartLoadWithRequest={requestHandler}
367
- onLoadEnd={onLoadEnd}
368
- javaScriptEnabled
369
- domStorageEnabled // To enable localStorage in Android webview
370
- webviewDebuggingEnabled
371
- bounces={false} // Controls the bouncing effect when scrolling past content boundaries (iOS only)
372
- scrollEnabled={true} // Enables scrolling within the WebView
373
- automaticallyAdjustContentInsets={false} // Disables automatic padding adjustments based on navigation bars/safe areas
374
- contentInsetAdjustmentBehavior="never" // Controls how the WebView adjusts its content layout relative to safe areas and system UI
375
- showsVerticalScrollIndicator={false}
376
- showsHorizontalScrollIndicator={false}
377
- testID="webview"
378
- {...(Platform.OS === 'ios'
379
- ? {
380
- decelerationRate: 'normal',
381
- keyboardDisplayRequiresUserAction: false,
382
- dataDetectorTypes: 'none',
383
- allowsInlineMediaPlayback: true,
384
- allowsBackForwardNavigationGestures: false,
385
- startInLoadingState: true,
386
- scrollEventThrottle: 16, // Optimize scroll performance
387
- overScrollMode: 'never', // Prevent overscroll effect
388
- }
389
- : {
390
- androidLayerType: 'hardware',
391
- cacheEnabled: true,
392
- cacheMode: 'LOAD_CACHE_ELSE_NETWORK',
393
- })}
394
- />
395
- </AndroidSafeAreaView>
396
- )
397
- }
461
+ )
398
462
 
399
463
  // Add styles for the WebView container
400
464
  const styles = StyleSheet.create({
package/src/utils/url.ts CHANGED
@@ -24,13 +24,13 @@ export const smartEncodeURIComponent = (str: string): string => {
24
24
 
25
25
  // If it's already encoded, return as is
26
26
  if (isEncoded(str)) {
27
- console.log('URL already encoded, skipping encoding:', str)
27
+ console.log('URL already encoded, skipping encoding')
28
28
  return str
29
29
  }
30
30
 
31
31
  // Otherwise, encode it
32
32
  const encoded = encodeURIComponent(str)
33
- console.log('URL encoded from:', str, 'to:', encoded)
33
+ console.log('URL encoded')
34
34
  return encoded
35
35
  }
36
36