@quiltt/react-native 4.5.1 → 5.0.1

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/dist/index.js CHANGED
@@ -1,650 +1,11 @@
1
1
  import { decode } from 'base-64';
2
2
  export * from '@quiltt/core';
3
- import { useQuilttSession, ConnectorSDKEventType } from '@quiltt/react';
4
- export { QuilttAuthProvider, QuilttProvider, QuilttSettingsProvider, useQuilttClient, useQuilttConnector, useQuilttSession, useQuilttSettings, useSession, useStorage } from '@quiltt/react';
5
- import { jsx, jsxs } from 'react/jsx-runtime';
6
- import { forwardRef, useRef, useState, useCallback, useMemo, useEffect, useImperativeHandle } from 'react';
7
- import { StyleSheet, StatusBar, Platform, SafeAreaView, View, Text, Pressable, ActivityIndicator, Linking } from 'react-native';
8
- import { URL } from 'react-native-url-polyfill';
9
- import { WebView } from 'react-native-webview';
10
- import { generateStackTrace, makeBacktrace, getCauses } from '@honeybadger-io/core/build/src/util';
11
-
12
- var version = "4.5.1";
13
-
14
- // Custom Error Reporter to avoid hooking into or colliding with a client's Honeybadger singleton
15
- const notifier = {
16
- name: 'Quiltt React Native SDK Reporter',
17
- url: 'https://www.quiltt.dev/connector/sdk/react-native',
18
- version: version
19
- };
20
- class ErrorReporter {
21
- constructor(platform){
22
- this.noticeUrl = 'https://api.honeybadger.io/v1/notices';
23
- this.apiKey = process.env.HONEYBADGER_API_KEY_REACT_NATIVE || '';
24
- this.clientName = 'react-native-sdk';
25
- this.clientVersion = version;
26
- this.platform = platform;
27
- this.logger = console;
28
- this.userAgent = `${this.clientName} ${this.clientVersion}; ${this.platform}`;
29
- }
30
- async notify(error, context) {
31
- const headers = {
32
- 'X-API-Key': this.apiKey,
33
- 'Content-Type': 'application/json',
34
- Accept: 'application/json',
35
- 'User-Agent': `${this.clientName} ${this.clientVersion}; ${this.platform}`
36
- };
37
- const payload = await this.buildPayload(error, context);
38
- const method = 'POST';
39
- const body = JSON.stringify(payload);
40
- const mode = 'cors';
41
- fetch(this.noticeUrl, {
42
- headers,
43
- method,
44
- body,
45
- mode
46
- }).then((response)=>{
47
- if (response.status !== 201) {
48
- this.logger.warn(`Error report failed: unknown response from server. code=${response.status}`);
49
- return;
50
- }
51
- return response.json();
52
- }).then((data)=>{
53
- if (data) {
54
- this.logger.info(`Error report sent ⚡ https://app.honeybadger.io/notice/${data?.id}`);
55
- }
56
- });
57
- }
58
- async buildPayload(error, localContext = {}) {
59
- const notice = error;
60
- notice.stack = generateStackTrace();
61
- notice.backtrace = makeBacktrace(notice.stack);
62
- return {
63
- notifier,
64
- error: {
65
- class: notice.name,
66
- message: notice.message,
67
- backtrace: notice.backtrace,
68
- // fingerprint: this.calculateFingerprint(notice),
69
- tags: notice.tags || [],
70
- causes: getCauses(notice, this.logger)
71
- },
72
- request: {
73
- url: notice.url,
74
- component: notice.component,
75
- action: notice.action,
76
- context: localContext || {},
77
- cgi_data: {},
78
- params: {},
79
- session: {}
80
- },
81
- server: {
82
- project_root: notice.projectRoot,
83
- environment_name: this.userAgent,
84
- revision: version,
85
- hostname: this.platform,
86
- time: new Date().toUTCString()
87
- },
88
- details: notice.details || {}
89
- };
90
- }
91
- }
92
-
93
- const getErrorMessage = (responseStatus, error)=>{
94
- if (error) return `An error occurred while checking the Connector URL: ${error?.name} \n${error?.message}`;
95
- return responseStatus ? `An error occurred loading the Connector. Response status: ${responseStatus}` : 'An error occurred while checking the Connector URL';
96
- };
97
-
98
- /**
99
- * Checks if a string appears to be already URL encoded
100
- * @param str The string to check
101
- * @returns boolean indicating if the string appears to be URL encoded
102
- */ const isEncoded = (str)=>{
103
- // Check for typical URL encoding patterns like %20, %3A, etc.
104
- const hasEncodedChars = /%[0-9A-F]{2}/i.test(str);
105
- // Check if double encoding has occurred (e.g., %253A instead of %3A)
106
- const hasDoubleEncoding = /%25[0-9A-F]{2}/i.test(str);
107
- // If we have encoded chars but no double encoding, it's likely properly encoded
108
- return hasEncodedChars && !hasDoubleEncoding;
109
- };
110
- /**
111
- * Smart URL encoder that ensures a string is encoded exactly once
112
- * @param str The string to encode
113
- * @returns A properly URL encoded string
114
- */ const smartEncodeURIComponent = (str)=>{
115
- if (!str) return str;
116
- // If it's already encoded, return as is
117
- if (isEncoded(str)) {
118
- console.log('URL already encoded, skipping encoding');
119
- return str;
120
- }
121
- // Otherwise, encode it
122
- const encoded = encodeURIComponent(str);
123
- console.log('URL encoded');
124
- return encoded;
125
- };
126
- /**
127
- * Checks if a string appears to be double-encoded
128
- */ const isDoubleEncoded = (str)=>{
129
- if (!str) return false;
130
- return /%25[0-9A-F]{2}/i.test(str);
131
- };
132
- /**
133
- * Normalizes a URL string by decoding it once if it appears to be double-encoded
134
- */ const normalizeUrlEncoding = (urlStr)=>{
135
- if (isDoubleEncoded(urlStr)) {
136
- console.log('Detected double-encoded URL:', urlStr);
137
- const normalized = decodeURIComponent(urlStr);
138
- console.log('Normalized to:', normalized);
139
- return normalized;
140
- }
141
- return urlStr;
142
- };
143
-
144
- const AndroidSafeAreaView = ({ testId, children })=>/*#__PURE__*/ jsx(SafeAreaView, {
145
- testID: testId,
146
- style: styles$2.AndroidSafeArea,
147
- children: children
148
- });
149
- const styles$2 = StyleSheet.create({
150
- AndroidSafeArea: {
151
- flex: 1,
152
- backgroundColor: 'white',
153
- paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0
154
- }
155
- });
156
-
157
- const ErrorScreen = ({ testId, error, cta })=>/*#__PURE__*/ jsx(AndroidSafeAreaView, {
158
- testId: testId,
159
- children: /*#__PURE__*/ jsxs(View, {
160
- style: [
161
- styles$1.container,
162
- styles$1.padding
163
- ],
164
- children: [
165
- /*#__PURE__*/ jsxs(View, {
166
- style: {
167
- flex: 1,
168
- justifyContent: 'center'
169
- },
170
- children: [
171
- /*#__PURE__*/ jsx(View, {
172
- style: {
173
- flexDirection: 'row',
174
- justifyContent: 'space-between',
175
- alignItems: 'center',
176
- marginVertical: 10
177
- },
178
- children: /*#__PURE__*/ jsx(Text, {
179
- style: [
180
- styles$1.title
181
- ],
182
- children: "Cannot connect to the internet."
183
- })
184
- }),
185
- /*#__PURE__*/ jsx(Text, {
186
- style: [
187
- styles$1.subtitle
188
- ],
189
- children: error
190
- })
191
- ]
192
- }),
193
- /*#__PURE__*/ jsx(Pressable, {
194
- style: [
195
- styles$1.pressable
196
- ],
197
- onPress: cta,
198
- children: /*#__PURE__*/ jsx(Text, {
199
- style: [
200
- styles$1.pressableText
201
- ],
202
- children: "Exit"
203
- })
204
- })
205
- ]
206
- })
207
- });
208
- const styles$1 = StyleSheet.create({
209
- container: {
210
- flex: 1,
211
- flexDirection: 'column',
212
- justifyContent: 'flex-start',
213
- alignItems: 'stretch',
214
- backgroundColor: '#F3F4F6'
215
- },
216
- title: {
217
- color: '#1F2937',
218
- fontSize: 30,
219
- fontWeight: 'bold'
220
- },
221
- subtitle: {
222
- color: 'rgba(107, 114, 128, 1)'
223
- },
224
- padding: {
225
- paddingHorizontal: 16,
226
- paddingVertical: 24
227
- },
228
- pressable: {
229
- marginTop: 20,
230
- backgroundColor: '#1F2937',
231
- padding: 10,
232
- paddingHorizontal: 25,
233
- borderRadius: 5
234
- },
235
- pressableText: {
236
- color: '#fff',
237
- textAlign: 'center'
238
- }
239
- });
240
-
241
- const LoadingScreen = ({ testId })=>/*#__PURE__*/ jsx(AndroidSafeAreaView, {
242
- testId: testId,
243
- children: /*#__PURE__*/ jsx(View, {
244
- style: {
245
- flex: 1,
246
- justifyContent: 'center',
247
- alignItems: 'center'
248
- },
249
- children: /*#__PURE__*/ jsx(ActivityIndicator, {
250
- testID: "activity-indicator",
251
- size: "large",
252
- color: "#5928A3"
253
- })
254
- })
255
- });
256
-
257
- const errorReporter = new ErrorReporter(`${Platform.OS} ${Platform.Version}`);
258
- const PREFLIGHT_RETRY_COUNT = 3;
259
- const parseMetadata = (url, connectorId)=>{
260
- const metadata = {
261
- connectorId: url.searchParams.get('connectorId') ?? connectorId
262
- };
263
- const profileId = url.searchParams.get('profileId');
264
- if (profileId) metadata.profileId = profileId;
265
- const connectionId = url.searchParams.get('connectionId');
266
- if (connectionId) metadata.connectionId = connectionId;
267
- const connectorSessionId = url.searchParams.get('connectorSession');
268
- if (connectorSessionId) metadata.connectorSession = {
269
- id: connectorSessionId
270
- };
271
- return metadata;
272
- };
273
- const checkConnectorUrl = async (connectorUrl, retryCount = 0)=>{
274
- let responseStatus;
275
- try {
276
- const response = await fetch(connectorUrl);
277
- switch(response.status){
278
- case 200:
279
- return {
280
- checked: true
281
- };
282
- case 400:
283
- console.log('Invalid configuration');
284
- return {
285
- checked: true
286
- };
287
- case 404:
288
- console.error('Connector not found');
289
- return {
290
- checked: true
291
- };
292
- default:
293
- throw new Error('Connector URL is not routable.');
294
- }
295
- } catch (error) {
296
- // Log error for debugging
297
- console.error(error);
298
- // Try again
299
- if (retryCount < PREFLIGHT_RETRY_COUNT) {
300
- const delay = 50 * 2 ** retryCount;
301
- await new Promise((resolve)=>setTimeout(resolve, delay));
302
- console.log(`Retrying connection... Attempt ${retryCount + 1}`);
303
- return checkConnectorUrl(connectorUrl, retryCount + 1);
304
- }
305
- // Report error after retries exhausted
306
- const errorMessage = getErrorMessage(responseStatus, error);
307
- const errorToSend = error || new Error(errorMessage);
308
- const context = {
309
- connectorUrl,
310
- responseStatus
311
- };
312
- await errorReporter.notify(errorToSend, context);
313
- // Return errored preflight check
314
- return {
315
- checked: true,
316
- error: errorMessage
317
- };
318
- }
319
- };
320
- /**
321
- * Handle opening OAuth URLs with proper encoding detection and normalization
322
- */ const handleOAuthUrl = (oauthUrl)=>{
323
- try {
324
- // Throw error if oauthUrl is null or undefined
325
- if (oauthUrl == null) {
326
- throw new Error('OAuth URL missing');
327
- }
328
- // Convert to string if it's a URL object
329
- const urlString = oauthUrl.toString();
330
- // Throw error if the resulting string is empty
331
- if (!urlString || urlString.trim() === '') {
332
- throw new Error('Empty OAuth URL');
333
- }
334
- // Normalize the URL encoding
335
- const normalizedUrl = normalizeUrlEncoding(urlString);
336
- // Open the normalized URL
337
- Linking.openURL(normalizedUrl);
338
- } catch (_error) {
339
- console.error('OAuth URL handling error');
340
- // Only try the fallback if oauthUrl is not null
341
- if (oauthUrl != null) {
342
- try {
343
- const fallbackUrl = typeof oauthUrl === 'string' ? oauthUrl : oauthUrl.toString();
344
- console.log('Attempting fallback OAuth opening');
345
- Linking.openURL(fallbackUrl);
346
- } catch (_fallbackError) {
347
- console.error('Fallback OAuth opening failed');
348
- }
349
- }
350
- }
351
- };
352
- const QuilttConnector = /*#__PURE__*/ forwardRef(({ connectorId, connectionId, institution, oauthRedirectUrl, onEvent, onLoad, onExit, onExitSuccess, onExitAbort, onExitError, testId }, ref)=>{
353
- const webViewRef = useRef(null);
354
- const { session } = useQuilttSession();
355
- const [preFlightCheck, setPreFlightCheck] = useState({
356
- checked: false
357
- });
358
- // Script to disable scrolling on header
359
- const disableHeaderScrollScript = `
360
- (function() {
361
- const header = document.querySelector('header');
362
- if (header) {
363
- header.style.position = 'fixed';
364
- header.style.top = '0';
365
- header.style.left = '0';
366
- header.style.right = '0';
367
- header.style.zIndex = '1000';
368
- }
369
- })();
370
- `;
371
- const onLoadEnd = useCallback(()=>{
372
- if (Platform.OS === 'ios') {
373
- webViewRef.current?.injectJavaScript(disableHeaderScrollScript);
374
- }
375
- }, []);
376
- // Ensure oauthRedirectUrl is encoded properly - only once
377
- const safeOAuthRedirectUrl = useMemo(()=>{
378
- return smartEncodeURIComponent(oauthRedirectUrl);
379
- }, [
380
- oauthRedirectUrl
381
- ]);
382
- const connectorUrl = useMemo(()=>{
383
- const url = new URL(`https://${connectorId}.quiltt.app`);
384
- // For normal parameters, just append them directly
385
- url.searchParams.append('mode', 'webview');
386
- url.searchParams.append('agent', `react-native-${version}`);
387
- // For the oauth_redirect_url, we need to be careful
388
- // If it's already encoded, we need to decode it once to prevent
389
- // the automatic encoding that happens with searchParams.append
390
- if (isEncoded(safeOAuthRedirectUrl)) {
391
- const decodedOnce = decodeURIComponent(safeOAuthRedirectUrl);
392
- url.searchParams.append('oauth_redirect_url', decodedOnce);
393
- } else {
394
- url.searchParams.append('oauth_redirect_url', safeOAuthRedirectUrl);
395
- }
396
- return url.toString();
397
- }, [
398
- connectorId,
399
- safeOAuthRedirectUrl
400
- ]);
401
- useEffect(()=>{
402
- if (preFlightCheck.checked) return;
403
- const fetchDataAndSetState = async ()=>{
404
- const connectorUrlStatus = await checkConnectorUrl(connectorUrl);
405
- setPreFlightCheck(connectorUrlStatus);
406
- };
407
- fetchDataAndSetState();
408
- }, [
409
- connectorUrl,
410
- preFlightCheck
411
- ]);
412
- const initInjectedJavaScript = useCallback(()=>{
413
- const script = `\
414
- const options = {\
415
- source: 'quiltt',\
416
- type: 'Options',\
417
- token: '${session?.token}',\
418
- connectorId: '${connectorId}',\
419
- connectionId: '${connectionId}',\
420
- institution: '${institution}', \
421
- };\
422
- const compactedOptions = Object.keys(options).reduce((acc, key) => {\
423
- if (options[key] !== 'undefined') {\
424
- acc[key] = options[key];\
425
- }\
426
- return acc;\
427
- }, {});\
428
- window.postMessage(compactedOptions);\
429
- `;
430
- webViewRef.current?.injectJavaScript(script);
431
- }, [
432
- connectionId,
433
- connectorId,
434
- institution,
435
- session?.token
436
- ]);
437
- const isQuilttEvent = useCallback((url)=>url.protocol === 'quilttconnector:', []);
438
- const shouldRender = useCallback((url)=>!isQuilttEvent(url), [
439
- isQuilttEvent
440
- ]);
441
- const clearLocalStorage = useCallback(()=>{
442
- const script = 'localStorage.clear();';
443
- webViewRef.current?.injectJavaScript(script);
444
- }, []);
445
- const handleQuilttEvent = useCallback((url)=>{
446
- url.searchParams.delete('source');
447
- url.searchParams.append('connectorId', connectorId);
448
- const metadata = parseMetadata(url, connectorId);
449
- requestAnimationFrame(()=>{
450
- const eventType = url.host;
451
- switch(eventType){
452
- case 'Load':
453
- console.log('Event: Load');
454
- initInjectedJavaScript();
455
- onEvent?.(ConnectorSDKEventType.Load, metadata);
456
- onLoad?.(metadata);
457
- break;
458
- case 'ExitAbort':
459
- console.log('Event: ExitAbort');
460
- clearLocalStorage();
461
- onEvent?.(ConnectorSDKEventType.ExitAbort, metadata);
462
- onExit?.(ConnectorSDKEventType.ExitAbort, metadata);
463
- onExitAbort?.(metadata);
464
- break;
465
- case 'ExitError':
466
- console.log('Event: ExitError');
467
- clearLocalStorage();
468
- onEvent?.(ConnectorSDKEventType.ExitError, metadata);
469
- onExit?.(ConnectorSDKEventType.ExitError, metadata);
470
- onExitError?.(metadata);
471
- break;
472
- case 'ExitSuccess':
473
- console.log('Event: ExitSuccess');
474
- clearLocalStorage();
475
- onEvent?.(ConnectorSDKEventType.ExitSuccess, metadata);
476
- onExit?.(ConnectorSDKEventType.ExitSuccess, metadata);
477
- onExitSuccess?.(metadata);
478
- break;
479
- case 'Authenticate':
480
- console.log('Event: Authenticate');
481
- break;
482
- case 'Navigate':
483
- {
484
- console.log('Event: Navigate');
485
- const navigateUrl = url.searchParams.get('url');
486
- if (navigateUrl) {
487
- if (isEncoded(navigateUrl)) {
488
- try {
489
- const decodedUrl = decodeURIComponent(navigateUrl);
490
- handleOAuthUrl(decodedUrl);
491
- } catch (_error) {
492
- console.error('Navigate URL decoding failed, using original');
493
- handleOAuthUrl(navigateUrl);
494
- }
495
- } else {
496
- handleOAuthUrl(navigateUrl);
497
- }
498
- } else {
499
- console.error('Navigate URL missing from request');
500
- }
501
- break;
502
- }
503
- // NOTE: The `OauthRequested` is deprecated and should not be used
504
- default:
505
- console.log(`Unhandled event: ${eventType}`);
506
- break;
507
- }
508
- });
509
- }, [
510
- clearLocalStorage,
511
- connectorId,
512
- initInjectedJavaScript,
513
- onEvent,
514
- onExit,
515
- onExitAbort,
516
- onExitError,
517
- onExitSuccess,
518
- onLoad
519
- ]);
520
- const requestHandler = useCallback((request)=>{
521
- const url = new URL(request.url);
522
- if (isQuilttEvent(url)) {
523
- handleQuilttEvent(url);
524
- return false;
525
- }
526
- if (shouldRender(url)) {
527
- return true;
528
- }
529
- // Plaid set oauth url by doing window.location.href = url
530
- // So we use `handleOAuthUrl` as a catch all and assume all url got to this step is Plaid OAuth url
531
- handleOAuthUrl(url);
532
- return false;
533
- }, [
534
- handleQuilttEvent,
535
- isQuilttEvent,
536
- shouldRender
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
- }), []);
576
- if (!preFlightCheck.checked) {
577
- return /*#__PURE__*/ jsx(LoadingScreen, {
578
- testId: "loading-screen"
579
- });
580
- }
581
- if (preFlightCheck.error) {
582
- return /*#__PURE__*/ jsx(ErrorScreen, {
583
- testId: "error-screen",
584
- error: preFlightCheck.error,
585
- cta: ()=>onExitError?.({
586
- connectorId
587
- })
588
- });
589
- }
590
- return /*#__PURE__*/ jsx(AndroidSafeAreaView, {
591
- testId: testId,
592
- children: /*#__PURE__*/ jsx(WebView, {
593
- ref: webViewRef,
594
- // Plaid keeps sending window.location = 'about:srcdoc' and causes some noise in RN
595
- domStorageEnabled: true,
596
- javaScriptEnabled: true,
597
- onLoadEnd: onLoadEnd,
598
- onShouldStartLoadWithRequest: requestHandler,
599
- originWhitelist: [
600
- '*'
601
- ],
602
- scrollEnabled: true,
603
- showsHorizontalScrollIndicator: false,
604
- showsVerticalScrollIndicator: false,
605
- source: {
606
- uri: connectorUrl
607
- },
608
- style: styles.webview,
609
- testID: "webview",
610
- webviewDebuggingEnabled: true,
611
- ...Platform.OS === 'ios' ? {
612
- allowsBackForwardNavigationGestures: false,
613
- allowsInlineMediaPlayback: true,
614
- automaticallyAdjustContentInsets: false,
615
- bounces: false,
616
- contentInsetAdjustmentBehavior: 'never',
617
- dataDetectorTypes: 'none',
618
- decelerationRate: 'normal',
619
- keyboardDisplayRequiresUserAction: false,
620
- scrollEventThrottle: 16,
621
- startInLoadingState: true
622
- } : {
623
- androidLayerType: 'hardware',
624
- cacheEnabled: true,
625
- cacheMode: 'LOAD_CACHE_ELSE_NETWORK',
626
- overScrollMode: 'never'
627
- }
628
- })
629
- });
630
- });
631
- // Add styles for the WebView container
632
- const styles = StyleSheet.create({
633
- webviewContainer: {
634
- flex: 1,
635
- backgroundColor: '#ffffff'
636
- },
637
- webview: {
638
- flex: 1,
639
- overflow: 'hidden'
640
- }
641
- });
642
- QuilttConnector.displayName = 'QuilttConnector';
3
+ export { QuilttSettingsProvider, useLazyQuery, useMutation, useQuery, useQuilttClient, useQuilttConnector, useQuilttSession, useQuilttSettings, useSession, useStorage, useSubscription } from '@quiltt/react';
4
+ export * from './components/index.js';
5
+ export * from './providers/index.js';
643
6
 
644
7
  // Hermes doesn't have atob
645
8
  // https://github.com/facebook/hermes/issues/1178
646
9
  if (!global.atob) {
647
10
  global.atob = decode;
648
11
  }
649
-
650
- export { QuilttConnector, checkConnectorUrl, handleOAuthUrl };