@quiltt/react-native 3.6.3 → 3.6.4

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,15 @@
1
1
  # @quiltt/react-native
2
2
 
3
+ ## 3.6.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [#246](https://github.com/quiltt/quiltt-js/pull/246) [`38f7904`](https://github.com/quiltt/quiltt-js/commit/38f79048e99dc617d700b62fd285623f9f2ae2fa) Thanks [@zubairaziz](https://github.com/zubairaziz)! - Update exports for react-native
8
+
9
+ - Updated dependencies [[`38f7904`](https://github.com/quiltt/quiltt-js/commit/38f79048e99dc617d700b62fd285623f9f2ae2fa)]:
10
+ - @quiltt/react@3.6.4
11
+ - @quiltt/core@3.6.4
12
+
3
13
  ## 3.6.3
4
14
 
5
15
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -5,103 +5,16 @@ var core = require('@quiltt/core');
5
5
  var react = require('@quiltt/react');
6
6
  var jsxRuntime = require('react/jsx-runtime');
7
7
  var react$1 = require('react');
8
- var reactNative = require('react-native');
9
8
  var reactNativeUrlPolyfill = require('react-native-url-polyfill');
10
9
  var reactNativeWebview = require('react-native-webview');
10
+ var reactNative = require('react-native');
11
11
  var util = require('@honeybadger-io/core/build/src/util');
12
12
 
13
- const ErrorReporterConfig = {
14
- honeybadger_api_key: 'undefined'
15
- };
16
-
17
13
  // Generated by genversion.
18
- const version = '3.6.3';
19
-
20
- // Quick hack to send error to Honeybadger to debug why the connector is not routable
21
- const notifier = {
22
- name: 'Quiltt React Native SDK Reporter',
23
- url: 'https://www.quiltt.dev/guides/connector/react-native',
24
- version: version
25
- };
26
- class ErrorReporter {
27
- constructor(platform){
28
- this.noticeUrl = 'https://api.honeybadger.io/v1/notices';
29
- this.apiKey = ErrorReporterConfig.honeybadger_api_key;
30
- this.clientName = 'react-native-sdk';
31
- this.clientVersion = version;
32
- this.platform = platform;
33
- this.logger = console;
34
- this.userAgent = `${this.clientName} ${this.clientVersion}; ${this.platform}`;
35
- }
36
- async send(error, context) {
37
- const headers = {
38
- 'X-API-Key': this.apiKey,
39
- 'Content-Type': 'application/json',
40
- Accept: 'application/json',
41
- 'User-Agent': `${this.clientName} ${this.clientVersion}; ${this.platform}`
42
- };
43
- const payload = await this.buildPayload(error, context);
44
- const method = 'POST';
45
- const body = JSON.stringify(payload);
46
- const mode = 'cors';
47
- fetch(this.noticeUrl, {
48
- headers,
49
- method,
50
- body,
51
- mode
52
- }).then((response)=>{
53
- if (response.status !== 201) {
54
- this.logger.warn(`Error report failed: unknown response from server. code=${response.status}`);
55
- return;
56
- }
57
- return response.json();
58
- }).then((data)=>{
59
- if (data) {
60
- this.logger.info(`Error report sent ⚡ https://app.honeybadger.io/notice/${data?.id}`);
61
- }
62
- });
63
- }
64
- async buildPayload(error, localContext = {}) {
65
- const notice = error;
66
- notice.stack = util.generateStackTrace();
67
- notice.backtrace = util.makeBacktrace(notice.stack);
68
- return {
69
- notifier,
70
- error: {
71
- class: notice.name,
72
- message: notice.message,
73
- backtrace: notice.backtrace,
74
- // fingerprint: this.calculateFingerprint(notice),
75
- tags: notice.tags || [],
76
- causes: util.getCauses(notice, this.logger)
77
- },
78
- request: {
79
- url: notice.url,
80
- component: notice.component,
81
- action: notice.action,
82
- context: localContext || {},
83
- cgi_data: {},
84
- params: {},
85
- session: {}
86
- },
87
- server: {
88
- project_root: notice.projectRoot,
89
- environment_name: this.userAgent,
90
- revision: version,
91
- hostname: this.platform,
92
- time: new Date().toUTCString()
93
- },
94
- details: notice.details || {}
95
- };
96
- }
97
- }
14
+ const version = '3.6.4';
98
15
 
99
- const getErrorMessage = (responseStatus, error)=>{
100
- if (error) return `An error occurred while checking the connector URL: ${error?.name} \n${error?.message}`;
101
- return responseStatus ? `The URL is not routable. Response status: ${responseStatus}` : 'An error occurred while checking the connector URL';
102
- };
103
-
104
- const AndroidSafeAreaView = ({ children })=>/*#__PURE__*/ jsxRuntime.jsx(reactNative.SafeAreaView, {
16
+ const AndroidSafeAreaView = ({ testId, children })=>/*#__PURE__*/ jsxRuntime.jsx(reactNative.SafeAreaView, {
17
+ testID: testId,
105
18
  style: styles$1.AndroidSafeArea,
106
19
  children: children
107
20
  });
@@ -113,7 +26,8 @@ const styles$1 = reactNative.StyleSheet.create({
113
26
  }
114
27
  });
115
28
 
116
- const ErrorScreen = ({ error, cta })=>/*#__PURE__*/ jsxRuntime.jsx(AndroidSafeAreaView, {
29
+ const ErrorScreen = ({ testId, error, cta })=>/*#__PURE__*/ jsxRuntime.jsx(AndroidSafeAreaView, {
30
+ testId: testId,
117
31
  children: /*#__PURE__*/ jsxRuntime.jsxs(reactNative.View, {
118
32
  style: [
119
33
  styles.container,
@@ -196,7 +110,8 @@ const styles = reactNative.StyleSheet.create({
196
110
  }
197
111
  });
198
112
 
199
- const LoadingScreen = ()=>/*#__PURE__*/ jsxRuntime.jsx(AndroidSafeAreaView, {
113
+ const LoadingScreen = ({ testId })=>/*#__PURE__*/ jsxRuntime.jsx(AndroidSafeAreaView, {
114
+ testId: testId,
200
115
  children: /*#__PURE__*/ jsxRuntime.jsx(reactNative.View, {
201
116
  style: {
202
117
  flex: 1,
@@ -204,15 +119,152 @@ const LoadingScreen = ()=>/*#__PURE__*/ jsxRuntime.jsx(AndroidSafeAreaView, {
204
119
  alignItems: 'center'
205
120
  },
206
121
  children: /*#__PURE__*/ jsxRuntime.jsx(reactNative.ActivityIndicator, {
122
+ testID: "activity-indicator",
207
123
  size: "large",
208
- color: "#0000ff"
124
+ color: "#5928A3"
209
125
  })
210
126
  })
211
127
  });
212
128
 
129
+ const ErrorReporterConfig = {
130
+ honeybadger_api_key: 'undefined'
131
+ };
132
+
133
+ // Quick hack to send error to Honeybadger to debug why the connector is not routable
134
+ const notifier = {
135
+ name: 'Quiltt React Native SDK Reporter',
136
+ url: 'https://www.quiltt.dev/guides/connector/react-native',
137
+ version: version
138
+ };
139
+ class ErrorReporter {
140
+ constructor(platform){
141
+ this.noticeUrl = 'https://api.honeybadger.io/v1/notices';
142
+ this.apiKey = ErrorReporterConfig.honeybadger_api_key;
143
+ this.clientName = 'react-native-sdk';
144
+ this.clientVersion = version;
145
+ this.platform = platform;
146
+ this.logger = console;
147
+ this.userAgent = `${this.clientName} ${this.clientVersion}; ${this.platform}`;
148
+ }
149
+ async send(error, context) {
150
+ const headers = {
151
+ 'X-API-Key': this.apiKey,
152
+ 'Content-Type': 'application/json',
153
+ Accept: 'application/json',
154
+ 'User-Agent': `${this.clientName} ${this.clientVersion}; ${this.platform}`
155
+ };
156
+ const payload = await this.buildPayload(error, context);
157
+ const method = 'POST';
158
+ const body = JSON.stringify(payload);
159
+ const mode = 'cors';
160
+ fetch(this.noticeUrl, {
161
+ headers,
162
+ method,
163
+ body,
164
+ mode
165
+ }).then((response)=>{
166
+ if (response.status !== 201) {
167
+ this.logger.warn(`Error report failed: unknown response from server. code=${response.status}`);
168
+ return;
169
+ }
170
+ return response.json();
171
+ }).then((data)=>{
172
+ if (data) {
173
+ this.logger.info(`Error report sent ⚡ https://app.honeybadger.io/notice/${data?.id}`);
174
+ }
175
+ });
176
+ }
177
+ async buildPayload(error, localContext = {}) {
178
+ const notice = error;
179
+ notice.stack = util.generateStackTrace();
180
+ notice.backtrace = util.makeBacktrace(notice.stack);
181
+ return {
182
+ notifier,
183
+ error: {
184
+ class: notice.name,
185
+ message: notice.message,
186
+ backtrace: notice.backtrace,
187
+ // fingerprint: this.calculateFingerprint(notice),
188
+ tags: notice.tags || [],
189
+ causes: util.getCauses(notice, this.logger)
190
+ },
191
+ request: {
192
+ url: notice.url,
193
+ component: notice.component,
194
+ action: notice.action,
195
+ context: localContext || {},
196
+ cgi_data: {},
197
+ params: {},
198
+ session: {}
199
+ },
200
+ server: {
201
+ project_root: notice.projectRoot,
202
+ environment_name: this.userAgent,
203
+ revision: version,
204
+ hostname: this.platform,
205
+ time: new Date().toUTCString()
206
+ },
207
+ details: notice.details || {}
208
+ };
209
+ }
210
+ }
211
+
212
+ const getErrorMessage = (responseStatus, error)=>{
213
+ if (error) return `An error occurred while checking the connector URL: ${error?.name} \n${error?.message}`;
214
+ return responseStatus ? `The URL is not routable. Response status: ${responseStatus}` : 'An error occurred while checking the connector URL';
215
+ };
216
+
213
217
  const errorReporter = new ErrorReporter(`${reactNative.Platform.OS} ${reactNative.Platform.Version}`);
214
218
  const PREFLIGHT_RETRY_COUNT = 3;
215
- const QuilttConnector = ({ connectorId, connectionId, institution, oauthRedirectUrl, onEvent, onLoad, onExit, onExitSuccess, onExitAbort, onExitError })=>{
219
+ const checkConnectorUrl = async (connectorUrl, retryCount = 0)=>{
220
+ let responseStatus;
221
+ let error;
222
+ let errorOccurred = false;
223
+ try {
224
+ const response = await fetch(connectorUrl);
225
+ if (!response.ok) {
226
+ console.error(`The URL ${connectorUrl} is not routable.`);
227
+ responseStatus = response.status;
228
+ errorOccurred = true;
229
+ } else {
230
+ console.log(`The URL ${connectorUrl} is routable.`);
231
+ return {
232
+ checked: true
233
+ };
234
+ }
235
+ } catch (e) {
236
+ error = e;
237
+ console.error(`An error occurred while checking the connector URL: ${error}`);
238
+ errorOccurred = true;
239
+ }
240
+ if (errorOccurred && retryCount < PREFLIGHT_RETRY_COUNT) {
241
+ const delay = 50 * Math.pow(2, retryCount);
242
+ await new Promise((resolve)=>setTimeout(resolve, delay));
243
+ console.log(`Retrying... Attempt number ${retryCount + 1}`);
244
+ return checkConnectorUrl(connectorUrl, retryCount + 1);
245
+ }
246
+ const errorMessage = getErrorMessage(responseStatus, error);
247
+ const errorToSend = error || new Error(errorMessage);
248
+ const context = {
249
+ connectorUrl,
250
+ responseStatus
251
+ };
252
+ if (responseStatus !== 404) await errorReporter.send(errorToSend, context);
253
+ return {
254
+ checked: true,
255
+ error: errorMessage
256
+ };
257
+ };
258
+
259
+ const handleOAuthUrl = (oauthUrl)=>{
260
+ if (oauthUrl.protocol !== 'https:') {
261
+ console.log(`handleOAuthUrl - Skipping non https url - ${oauthUrl.href}`);
262
+ return;
263
+ }
264
+ reactNative.Linking.openURL(oauthUrl.href);
265
+ };
266
+
267
+ const QuilttConnector = ({ testId, connectorId, connectionId, institution, oauthRedirectUrl, onEvent, onLoad, onExit, onExitSuccess, onExitAbort, onExitError })=>{
216
268
  const webViewRef = react$1.useRef(null);
217
269
  const { session } = react.useQuilttSession();
218
270
  const encodedOAuthRedirectUrl = react$1.useMemo(()=>encodeURIComponent(oauthRedirectUrl), [
@@ -231,59 +283,15 @@ const QuilttConnector = ({ connectorId, connectionId, institution, oauthRedirect
231
283
  const [preFlightCheck, setPreFlightCheck] = react$1.useState({
232
284
  checked: false
233
285
  });
234
- const checkConnectorUrl = react$1.useCallback(async (retryCount = 0)=>{
235
- let responseStatus;
236
- let error;
237
- let errorOccurred = false;
238
- try {
239
- const response = await fetch(connectorUrl);
240
- if (!response.ok) {
241
- console.error(`The URL ${connectorUrl} is not routable.`);
242
- responseStatus = response.status;
243
- errorOccurred = true;
244
- } else {
245
- console.log(`The URL ${connectorUrl} is routable.`);
246
- return {
247
- checked: true
248
- };
249
- }
250
- } catch (e) {
251
- error = e;
252
- console.error(`An error occurred while checking the connector URL: ${error}`);
253
- errorOccurred = true;
254
- }
255
- // Retry logic in case of error or response not OK
256
- if (errorOccurred && retryCount < PREFLIGHT_RETRY_COUNT) {
257
- const delay = 50 * Math.pow(2, retryCount) // Exponential back-off
258
- ;
259
- await new Promise((resolve)=>setTimeout(resolve, delay)) // delay with exponential back-off for each retry
260
- ;
261
- console.log(`Retrying... Attempt number ${retryCount + 1}`);
262
- return checkConnectorUrl(retryCount + 1);
263
- }
264
- const errorMessage = getErrorMessage(responseStatus, error);
265
- const errorToSend = error || new Error(errorMessage);
266
- const context = {
267
- connectorUrl,
268
- responseStatus
269
- };
270
- if (responseStatus !== 404) errorReporter.send(errorToSend, context);
271
- return {
272
- checked: true,
273
- error: errorMessage
274
- };
275
- }, [
276
- connectorUrl
277
- ]);
278
286
  react$1.useEffect(()=>{
279
287
  if (preFlightCheck.checked) return;
280
288
  const fetchDataAndSetState = async ()=>{
281
- const connectorUrlStatus = await checkConnectorUrl();
289
+ const connectorUrlStatus = await checkConnectorUrl(connectorUrl);
282
290
  setPreFlightCheck(connectorUrlStatus);
283
291
  };
284
292
  fetchDataAndSetState();
285
293
  }, [
286
- checkConnectorUrl,
294
+ connectorUrl,
287
295
  preFlightCheck
288
296
  ]);
289
297
  const initInjectedJavaScript = react$1.useCallback(()=>{
@@ -313,7 +321,7 @@ const QuilttConnector = ({ connectorId, connectionId, institution, oauthRedirect
313
321
  ]);
314
322
  // allowedListUrl & shouldRender ensure we are only rendering Quiltt, MX and Plaid content in Webview
315
323
  // For other urls, we assume those are bank urls, which needs to be handle in external browser.
316
- // @todo Convert it to a list from Quiltt Server to prevent MX/ Plaid changes.
324
+ // TODO: Convert it to a list from Quiltt Server to prevent MX/ Plaid changes.
317
325
  const allowedListUrl = react$1.useMemo(()=>[
318
326
  'quiltt.app',
319
327
  'quiltt.dev',
@@ -335,13 +343,6 @@ const QuilttConnector = ({ connectorId, connectionId, institution, oauthRedirect
335
343
  const script = 'localStorage.clear();';
336
344
  webViewRef.current?.injectJavaScript(script);
337
345
  };
338
- const handleOAuthUrl = react$1.useCallback((oauthUrl)=>{
339
- if (oauthUrl.protocol !== 'https:') {
340
- console.log(`handleOAuthUrl - Skipping non https url - ${oauthUrl.href}`);
341
- return;
342
- }
343
- reactNative.Linking.openURL(oauthUrl.href);
344
- }, []);
345
346
  const handleQuilttEvent = react$1.useCallback((url)=>{
346
347
  url.searchParams.delete('source');
347
348
  url.searchParams.append('connectorId', connectorId);
@@ -382,7 +383,6 @@ const QuilttConnector = ({ connectorId, connectionId, institution, oauthRedirect
382
383
  }
383
384
  }, [
384
385
  connectorId,
385
- handleOAuthUrl,
386
386
  initInjectedJavaScript,
387
387
  onEvent,
388
388
  onExit,
@@ -403,22 +403,26 @@ const QuilttConnector = ({ connectorId, connectionId, institution, oauthRedirect
403
403
  handleOAuthUrl(url);
404
404
  return false;
405
405
  }, [
406
- handleOAuthUrl,
407
406
  handleQuilttEvent,
408
407
  isQuilttEvent,
409
408
  shouldRender
410
409
  ]);
411
- if (!preFlightCheck.checked) return /*#__PURE__*/ jsxRuntime.jsx(LoadingScreen, {});
410
+ if (!preFlightCheck.checked) return /*#__PURE__*/ jsxRuntime.jsx(LoadingScreen, {
411
+ testId: "loading-screen"
412
+ });
412
413
  if (preFlightCheck.error) return /*#__PURE__*/ jsxRuntime.jsx(ErrorScreen, {
414
+ testId: "error-screen",
413
415
  error: preFlightCheck.error,
414
416
  cta: ()=>onExitError?.({
415
417
  connectorId
416
418
  })
417
419
  });
418
420
  return /*#__PURE__*/ jsxRuntime.jsx(AndroidSafeAreaView, {
421
+ testId: testId,
419
422
  children: /*#__PURE__*/ jsxRuntime.jsx(reactNativeWebview.WebView, {
423
+ testID: "webview",
420
424
  ref: webViewRef,
421
- // Plaid keep sending window.location = 'about:srcdoc' and causes some noise in RN
425
+ // Plaid keeps sending window.location = 'about:srcdoc' and causes some noise in RN
422
426
  // All whitelists are now handled in requestHandler, handleQuilttEvent and handleOAuthUrl
423
427
  originWhitelist: [
424
428
  '*'
package/dist/index.d.cts CHANGED
@@ -4,13 +4,14 @@ export { QuilttAuthProvider, QuilttProvider, QuilttSettingsProvider, useQuilttCl
4
4
  import * as react_jsx_runtime from 'react/jsx-runtime';
5
5
 
6
6
  type QuilttConnectorProps = {
7
+ testId?: string;
7
8
  connectorId: string;
8
9
  connectionId?: string;
9
10
  institution?: string;
10
11
  oauthRedirectUrl: string;
11
12
  } & ConnectorSDKCallbacks;
12
13
  declare const QuilttConnector: {
13
- ({ connectorId, connectionId, institution, oauthRedirectUrl, onEvent, onLoad, onExit, onExitSuccess, onExitAbort, onExitError, }: QuilttConnectorProps): react_jsx_runtime.JSX.Element;
14
+ ({ testId, connectorId, connectionId, institution, oauthRedirectUrl, onEvent, onLoad, onExit, onExitSuccess, onExitAbort, onExitError, }: QuilttConnectorProps): react_jsx_runtime.JSX.Element;
14
15
  displayName: string;
15
16
  };
16
17
 
package/dist/index.d.ts CHANGED
@@ -4,13 +4,14 @@ export { QuilttAuthProvider, QuilttProvider, QuilttSettingsProvider, useQuilttCl
4
4
  import * as react_jsx_runtime from 'react/jsx-runtime';
5
5
 
6
6
  type QuilttConnectorProps = {
7
+ testId?: string;
7
8
  connectorId: string;
8
9
  connectionId?: string;
9
10
  institution?: string;
10
11
  oauthRedirectUrl: string;
11
12
  } & ConnectorSDKCallbacks;
12
13
  declare const QuilttConnector: {
13
- ({ connectorId, connectionId, institution, oauthRedirectUrl, onEvent, onLoad, onExit, onExitSuccess, onExitAbort, onExitError, }: QuilttConnectorProps): react_jsx_runtime.JSX.Element;
14
+ ({ testId, connectorId, connectionId, institution, oauthRedirectUrl, onEvent, onLoad, onExit, onExitSuccess, onExitAbort, onExitError, }: QuilttConnectorProps): react_jsx_runtime.JSX.Element;
14
15
  displayName: string;
15
16
  };
16
17
 
package/dist/index.js ADDED
@@ -0,0 +1,447 @@
1
+ import { decode } from 'base-64';
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 { useRef, useMemo, useState, useEffect, useCallback } from 'react';
7
+ import { URL } from 'react-native-url-polyfill';
8
+ import { WebView } from 'react-native-webview';
9
+ import { StyleSheet, Platform, StatusBar, SafeAreaView, View, Text, Pressable, ActivityIndicator, Linking } from 'react-native';
10
+ import { generateStackTrace, makeBacktrace, getCauses } from '@honeybadger-io/core/build/src/util';
11
+
12
+ // Generated by genversion.
13
+ const version = '3.6.4';
14
+
15
+ const AndroidSafeAreaView = ({ testId, children })=>/*#__PURE__*/ jsx(SafeAreaView, {
16
+ testID: testId,
17
+ style: styles$1.AndroidSafeArea,
18
+ children: children
19
+ });
20
+ const styles$1 = StyleSheet.create({
21
+ AndroidSafeArea: {
22
+ flex: 1,
23
+ backgroundColor: 'white',
24
+ paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0
25
+ }
26
+ });
27
+
28
+ const ErrorScreen = ({ testId, error, cta })=>/*#__PURE__*/ jsx(AndroidSafeAreaView, {
29
+ testId: testId,
30
+ children: /*#__PURE__*/ jsxs(View, {
31
+ style: [
32
+ styles.container,
33
+ styles.padding
34
+ ],
35
+ children: [
36
+ /*#__PURE__*/ jsxs(View, {
37
+ style: {
38
+ flex: 1,
39
+ justifyContent: 'center'
40
+ },
41
+ children: [
42
+ /*#__PURE__*/ jsx(View, {
43
+ style: {
44
+ flexDirection: 'row',
45
+ justifyContent: 'space-between',
46
+ alignItems: 'center',
47
+ marginVertical: 10
48
+ },
49
+ children: /*#__PURE__*/ jsx(Text, {
50
+ style: [
51
+ styles.title
52
+ ],
53
+ children: "Cannot connect to the internet."
54
+ })
55
+ }),
56
+ /*#__PURE__*/ jsx(Text, {
57
+ style: [
58
+ styles.subtitle
59
+ ],
60
+ children: error
61
+ })
62
+ ]
63
+ }),
64
+ /*#__PURE__*/ jsx(Pressable, {
65
+ style: [
66
+ styles.pressable
67
+ ],
68
+ onPress: cta,
69
+ children: /*#__PURE__*/ jsx(Text, {
70
+ style: [
71
+ styles.pressableText
72
+ ],
73
+ children: "Exit"
74
+ })
75
+ })
76
+ ]
77
+ })
78
+ });
79
+ const styles = StyleSheet.create({
80
+ container: {
81
+ flex: 1,
82
+ flexDirection: 'column',
83
+ justifyContent: 'flex-start',
84
+ alignItems: 'stretch',
85
+ backgroundColor: '#F3F4F6'
86
+ },
87
+ title: {
88
+ color: '#1F2937',
89
+ fontSize: 30,
90
+ fontWeight: 'bold'
91
+ },
92
+ subtitle: {
93
+ color: 'rgba(107, 114, 128, 1)'
94
+ },
95
+ padding: {
96
+ paddingHorizontal: 16,
97
+ paddingVertical: 24
98
+ },
99
+ pressable: {
100
+ marginTop: 20,
101
+ backgroundColor: '#1F2937',
102
+ padding: 10,
103
+ paddingHorizontal: 25,
104
+ borderRadius: 5
105
+ },
106
+ pressableText: {
107
+ color: '#fff',
108
+ textAlign: 'center'
109
+ }
110
+ });
111
+
112
+ const LoadingScreen = ({ testId })=>/*#__PURE__*/ jsx(AndroidSafeAreaView, {
113
+ testId: testId,
114
+ children: /*#__PURE__*/ jsx(View, {
115
+ style: {
116
+ flex: 1,
117
+ justifyContent: 'center',
118
+ alignItems: 'center'
119
+ },
120
+ children: /*#__PURE__*/ jsx(ActivityIndicator, {
121
+ testID: "activity-indicator",
122
+ size: "large",
123
+ color: "#5928A3"
124
+ })
125
+ })
126
+ });
127
+
128
+ const ErrorReporterConfig = {
129
+ honeybadger_api_key: 'undefined'
130
+ };
131
+
132
+ // Quick hack to send error to Honeybadger to debug why the connector is not routable
133
+ const notifier = {
134
+ name: 'Quiltt React Native SDK Reporter',
135
+ url: 'https://www.quiltt.dev/guides/connector/react-native',
136
+ version: version
137
+ };
138
+ class ErrorReporter {
139
+ constructor(platform){
140
+ this.noticeUrl = 'https://api.honeybadger.io/v1/notices';
141
+ this.apiKey = ErrorReporterConfig.honeybadger_api_key;
142
+ this.clientName = 'react-native-sdk';
143
+ this.clientVersion = version;
144
+ this.platform = platform;
145
+ this.logger = console;
146
+ this.userAgent = `${this.clientName} ${this.clientVersion}; ${this.platform}`;
147
+ }
148
+ async send(error, context) {
149
+ const headers = {
150
+ 'X-API-Key': this.apiKey,
151
+ 'Content-Type': 'application/json',
152
+ Accept: 'application/json',
153
+ 'User-Agent': `${this.clientName} ${this.clientVersion}; ${this.platform}`
154
+ };
155
+ const payload = await this.buildPayload(error, context);
156
+ const method = 'POST';
157
+ const body = JSON.stringify(payload);
158
+ const mode = 'cors';
159
+ fetch(this.noticeUrl, {
160
+ headers,
161
+ method,
162
+ body,
163
+ mode
164
+ }).then((response)=>{
165
+ if (response.status !== 201) {
166
+ this.logger.warn(`Error report failed: unknown response from server. code=${response.status}`);
167
+ return;
168
+ }
169
+ return response.json();
170
+ }).then((data)=>{
171
+ if (data) {
172
+ this.logger.info(`Error report sent ⚡ https://app.honeybadger.io/notice/${data?.id}`);
173
+ }
174
+ });
175
+ }
176
+ async buildPayload(error, localContext = {}) {
177
+ const notice = error;
178
+ notice.stack = generateStackTrace();
179
+ notice.backtrace = makeBacktrace(notice.stack);
180
+ return {
181
+ notifier,
182
+ error: {
183
+ class: notice.name,
184
+ message: notice.message,
185
+ backtrace: notice.backtrace,
186
+ // fingerprint: this.calculateFingerprint(notice),
187
+ tags: notice.tags || [],
188
+ causes: getCauses(notice, this.logger)
189
+ },
190
+ request: {
191
+ url: notice.url,
192
+ component: notice.component,
193
+ action: notice.action,
194
+ context: localContext || {},
195
+ cgi_data: {},
196
+ params: {},
197
+ session: {}
198
+ },
199
+ server: {
200
+ project_root: notice.projectRoot,
201
+ environment_name: this.userAgent,
202
+ revision: version,
203
+ hostname: this.platform,
204
+ time: new Date().toUTCString()
205
+ },
206
+ details: notice.details || {}
207
+ };
208
+ }
209
+ }
210
+
211
+ const getErrorMessage = (responseStatus, error)=>{
212
+ if (error) return `An error occurred while checking the connector URL: ${error?.name} \n${error?.message}`;
213
+ return responseStatus ? `The URL is not routable. Response status: ${responseStatus}` : 'An error occurred while checking the connector URL';
214
+ };
215
+
216
+ const errorReporter = new ErrorReporter(`${Platform.OS} ${Platform.Version}`);
217
+ const PREFLIGHT_RETRY_COUNT = 3;
218
+ const checkConnectorUrl = async (connectorUrl, retryCount = 0)=>{
219
+ let responseStatus;
220
+ let error;
221
+ let errorOccurred = false;
222
+ try {
223
+ const response = await fetch(connectorUrl);
224
+ if (!response.ok) {
225
+ console.error(`The URL ${connectorUrl} is not routable.`);
226
+ responseStatus = response.status;
227
+ errorOccurred = true;
228
+ } else {
229
+ console.log(`The URL ${connectorUrl} is routable.`);
230
+ return {
231
+ checked: true
232
+ };
233
+ }
234
+ } catch (e) {
235
+ error = e;
236
+ console.error(`An error occurred while checking the connector URL: ${error}`);
237
+ errorOccurred = true;
238
+ }
239
+ if (errorOccurred && retryCount < PREFLIGHT_RETRY_COUNT) {
240
+ const delay = 50 * Math.pow(2, retryCount);
241
+ await new Promise((resolve)=>setTimeout(resolve, delay));
242
+ console.log(`Retrying... Attempt number ${retryCount + 1}`);
243
+ return checkConnectorUrl(connectorUrl, retryCount + 1);
244
+ }
245
+ const errorMessage = getErrorMessage(responseStatus, error);
246
+ const errorToSend = error || new Error(errorMessage);
247
+ const context = {
248
+ connectorUrl,
249
+ responseStatus
250
+ };
251
+ if (responseStatus !== 404) await errorReporter.send(errorToSend, context);
252
+ return {
253
+ checked: true,
254
+ error: errorMessage
255
+ };
256
+ };
257
+
258
+ const handleOAuthUrl = (oauthUrl)=>{
259
+ if (oauthUrl.protocol !== 'https:') {
260
+ console.log(`handleOAuthUrl - Skipping non https url - ${oauthUrl.href}`);
261
+ return;
262
+ }
263
+ Linking.openURL(oauthUrl.href);
264
+ };
265
+
266
+ const QuilttConnector = ({ testId, connectorId, connectionId, institution, oauthRedirectUrl, onEvent, onLoad, onExit, onExitSuccess, onExitAbort, onExitError })=>{
267
+ const webViewRef = useRef(null);
268
+ const { session } = useQuilttSession();
269
+ const encodedOAuthRedirectUrl = useMemo(()=>encodeURIComponent(oauthRedirectUrl), [
270
+ oauthRedirectUrl
271
+ ]);
272
+ const connectorUrl = useMemo(()=>{
273
+ const url = new URL(`https://${connectorId}.quiltt.app`);
274
+ url.searchParams.append('mode', 'webview');
275
+ url.searchParams.append('oauth_redirect_url', encodedOAuthRedirectUrl);
276
+ url.searchParams.append('agent', `react-native-${version}`);
277
+ return url.toString();
278
+ }, [
279
+ connectorId,
280
+ encodedOAuthRedirectUrl
281
+ ]);
282
+ const [preFlightCheck, setPreFlightCheck] = useState({
283
+ checked: false
284
+ });
285
+ useEffect(()=>{
286
+ if (preFlightCheck.checked) return;
287
+ const fetchDataAndSetState = async ()=>{
288
+ const connectorUrlStatus = await checkConnectorUrl(connectorUrl);
289
+ setPreFlightCheck(connectorUrlStatus);
290
+ };
291
+ fetchDataAndSetState();
292
+ }, [
293
+ connectorUrl,
294
+ preFlightCheck
295
+ ]);
296
+ const initInjectedJavaScript = useCallback(()=>{
297
+ const script = `\
298
+ const options = {\
299
+ source: 'quiltt',\
300
+ type: 'Options',\
301
+ token: '${session?.token}',\
302
+ connectorId: '${connectorId}',\
303
+ connectionId: '${connectionId}',\
304
+ institution: '${institution}', \
305
+ };\
306
+ const compactedOptions = Object.keys(options).reduce((acc, key) => {\
307
+ if (options[key] !== 'undefined') {\
308
+ acc[key] = options[key];\
309
+ }\
310
+ return acc;\
311
+ }, {});\
312
+ window.postMessage(compactedOptions);\
313
+ `;
314
+ webViewRef.current?.injectJavaScript(script);
315
+ }, [
316
+ connectionId,
317
+ connectorId,
318
+ institution,
319
+ session?.token
320
+ ]);
321
+ // allowedListUrl & shouldRender ensure we are only rendering Quiltt, MX and Plaid content in Webview
322
+ // For other urls, we assume those are bank urls, which needs to be handle in external browser.
323
+ // TODO: Convert it to a list from Quiltt Server to prevent MX/ Plaid changes.
324
+ const allowedListUrl = useMemo(()=>[
325
+ 'quiltt.app',
326
+ 'quiltt.dev',
327
+ 'moneydesktop.com',
328
+ 'cdn.plaid.com/link/v2/stable/link.html'
329
+ ], []);
330
+ const isQuilttEvent = useCallback((url)=>url.protocol === 'quilttconnector:', []);
331
+ const shouldRender = useCallback((url)=>{
332
+ if (isQuilttEvent(url)) return false;
333
+ if (url.protocol !== 'https:') {
334
+ return false;
335
+ }
336
+ return allowedListUrl.some((href)=>url.href.includes(href));
337
+ }, [
338
+ allowedListUrl,
339
+ isQuilttEvent
340
+ ]);
341
+ const clearLocalStorage = ()=>{
342
+ const script = 'localStorage.clear();';
343
+ webViewRef.current?.injectJavaScript(script);
344
+ };
345
+ const handleQuilttEvent = useCallback((url)=>{
346
+ url.searchParams.delete('source');
347
+ url.searchParams.append('connectorId', connectorId);
348
+ const metadata = Object.fromEntries(url.searchParams);
349
+ const eventType = url.host;
350
+ switch(eventType){
351
+ case 'Load':
352
+ initInjectedJavaScript();
353
+ onEvent?.(ConnectorSDKEventType.Load, metadata);
354
+ onLoad?.(metadata);
355
+ break;
356
+ case 'ExitAbort':
357
+ clearLocalStorage();
358
+ onEvent?.(ConnectorSDKEventType.ExitAbort, metadata);
359
+ onExit?.(ConnectorSDKEventType.ExitAbort, metadata);
360
+ onExitAbort?.(metadata);
361
+ break;
362
+ case 'ExitError':
363
+ clearLocalStorage();
364
+ onEvent?.(ConnectorSDKEventType.ExitError, metadata);
365
+ onExit?.(ConnectorSDKEventType.ExitError, metadata);
366
+ onExitError?.(metadata);
367
+ break;
368
+ case 'ExitSuccess':
369
+ clearLocalStorage();
370
+ onEvent?.(ConnectorSDKEventType.ExitSuccess, metadata);
371
+ onExit?.(ConnectorSDKEventType.ExitSuccess, metadata);
372
+ onExitSuccess?.(metadata);
373
+ break;
374
+ case 'Authenticate':
375
+ break;
376
+ case 'OauthRequested':
377
+ handleOAuthUrl(new URL(url.searchParams.get('oauthUrl')));
378
+ break;
379
+ default:
380
+ console.log('unhandled event', url);
381
+ break;
382
+ }
383
+ }, [
384
+ connectorId,
385
+ initInjectedJavaScript,
386
+ onEvent,
387
+ onExit,
388
+ onExitAbort,
389
+ onExitError,
390
+ onExitSuccess,
391
+ onLoad
392
+ ]);
393
+ const requestHandler = useCallback((request)=>{
394
+ const url = new URL(request.url);
395
+ if (isQuilttEvent(url)) {
396
+ handleQuilttEvent(url);
397
+ return false;
398
+ }
399
+ if (shouldRender(url)) return true;
400
+ // Plaid set oauth url by doing window.location.href = url
401
+ // So we use `handleOAuthUrl` as a catch all and assume all url got to this step is Plaid OAuth url
402
+ handleOAuthUrl(url);
403
+ return false;
404
+ }, [
405
+ handleQuilttEvent,
406
+ isQuilttEvent,
407
+ shouldRender
408
+ ]);
409
+ if (!preFlightCheck.checked) return /*#__PURE__*/ jsx(LoadingScreen, {
410
+ testId: "loading-screen"
411
+ });
412
+ if (preFlightCheck.error) return /*#__PURE__*/ jsx(ErrorScreen, {
413
+ testId: "error-screen",
414
+ error: preFlightCheck.error,
415
+ cta: ()=>onExitError?.({
416
+ connectorId
417
+ })
418
+ });
419
+ return /*#__PURE__*/ jsx(AndroidSafeAreaView, {
420
+ testId: testId,
421
+ children: /*#__PURE__*/ jsx(WebView, {
422
+ testID: "webview",
423
+ ref: webViewRef,
424
+ // Plaid keeps sending window.location = 'about:srcdoc' and causes some noise in RN
425
+ // All whitelists are now handled in requestHandler, handleQuilttEvent and handleOAuthUrl
426
+ originWhitelist: [
427
+ '*'
428
+ ],
429
+ source: {
430
+ uri: connectorUrl
431
+ },
432
+ onShouldStartLoadWithRequest: requestHandler,
433
+ javaScriptEnabled: true,
434
+ domStorageEnabled: true,
435
+ webviewDebuggingEnabled: true
436
+ })
437
+ });
438
+ };
439
+ QuilttConnector.displayName = 'QuilttConnector';
440
+
441
+ // Hermes doesn't have atob
442
+ // https://github.com/facebook/hermes/issues/1178
443
+ if (!global.atob) {
444
+ global.atob = decode;
445
+ }
446
+
447
+ export { QuilttConnector };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quiltt/react-native",
3
- "version": "3.6.3",
3
+ "version": "3.6.4",
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": {
@@ -11,12 +11,14 @@
11
11
  "license": "MIT",
12
12
  "type": "module",
13
13
  "exports": {
14
+ "import": {
15
+ "default": "./dist/index.js"
16
+ },
14
17
  "require": {
15
- "import": "./dist/index.cjs",
16
- "types": "./dist/index.d.ts"
18
+ "default": "./dist/index.cjs"
17
19
  }
18
20
  },
19
- "main": "./dist/index.cjs",
21
+ "main": "./dist/index.js",
20
22
  "types": "./dist/index.d.ts",
21
23
  "files": [
22
24
  "dist/**",
@@ -25,14 +27,14 @@
25
27
  ],
26
28
  "dependencies": {
27
29
  "@honeybadger-io/core": "6.6.0",
28
- "@quiltt/react": "3.6.3",
29
- "@quiltt/core": "3.6.3"
30
+ "@quiltt/core": "3.6.4",
31
+ "@quiltt/react": "3.6.4"
30
32
  },
31
33
  "devDependencies": {
32
34
  "@apollo/client": "3.9.9",
33
35
  "@trivago/prettier-plugin-sort-imports": "4.1.1",
34
36
  "@types/base-64": "0.1.0",
35
- "@types/node": "20.12.2",
37
+ "@types/node": "20.12.7",
36
38
  "@types/react": "18.2.73",
37
39
  "@types/react-native": "0.72.5",
38
40
  "@typescript-eslint/eslint-plugin": "5.60.1",
@@ -2,8 +2,14 @@ import { PropsWithChildren } from 'react'
2
2
 
3
3
  import { SafeAreaView, StyleSheet, Platform, StatusBar } from 'react-native'
4
4
 
5
- export const AndroidSafeAreaView = ({ children }: PropsWithChildren) => (
6
- <SafeAreaView style={styles.AndroidSafeArea}>{children}</SafeAreaView>
5
+ type AndroidSafeAreaViewProps = PropsWithChildren & {
6
+ testId?: string
7
+ }
8
+
9
+ export const AndroidSafeAreaView = ({ testId, children }: AndroidSafeAreaViewProps) => (
10
+ <SafeAreaView testID={testId} style={styles.AndroidSafeArea}>
11
+ {children}
12
+ </SafeAreaView>
7
13
  )
8
14
 
9
15
  const styles = StyleSheet.create({
@@ -2,13 +2,14 @@ import { View, Text, Pressable, StyleSheet } from 'react-native'
2
2
 
3
3
  import { AndroidSafeAreaView } from './AndroidSafeAreaView'
4
4
 
5
- type ErrorScreenProp = {
5
+ type ErrorScreenProps = {
6
+ testId?: string
6
7
  error: string
7
8
  cta: () => void
8
9
  }
9
10
 
10
- export const ErrorScreen = ({ error, cta }: ErrorScreenProp) => (
11
- <AndroidSafeAreaView>
11
+ export const ErrorScreen = ({ testId, error, cta }: ErrorScreenProps) => (
12
+ <AndroidSafeAreaView testId={testId}>
12
13
  <View style={[styles.container, styles.padding]}>
13
14
  <View style={{ flex: 1, justifyContent: 'center' }}>
14
15
  <View
@@ -2,10 +2,14 @@ import { ActivityIndicator, View } from 'react-native'
2
2
 
3
3
  import { AndroidSafeAreaView } from './AndroidSafeAreaView'
4
4
 
5
- export const LoadingScreen = () => (
6
- <AndroidSafeAreaView>
5
+ type LoadingScreenProps = {
6
+ testId?: string
7
+ }
8
+
9
+ export const LoadingScreen = ({ testId }: LoadingScreenProps) => (
10
+ <AndroidSafeAreaView testId={testId}>
7
11
  <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
8
- <ActivityIndicator size="large" color="#0000ff" />
12
+ <ActivityIndicator testID="activity-indicator" size="large" color="#5928A3" />
9
13
  </View>
10
14
  </AndroidSafeAreaView>
11
15
  )
@@ -1,6 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
2
 
3
- import { Linking, Platform } from 'react-native'
4
3
  // React Native's URL implementation is incomplete
5
4
  // https://github.com/facebook/react-native/issues/16434
6
5
  import { URL } from 'react-native-url-polyfill'
@@ -14,29 +13,23 @@ import {
14
13
  useQuilttSession,
15
14
  } from '@quiltt/react'
16
15
 
17
- import { ErrorReporter, getErrorMessage } from '../utils'
18
- import { version } from '../version'
16
+ import { version } from '@/version'
19
17
  import { AndroidSafeAreaView } from './AndroidSafeAreaView'
20
18
  import { ErrorScreen } from './ErrorScreen'
21
19
  import { LoadingScreen } from './LoadingScreen'
22
-
23
- const errorReporter = new ErrorReporter(`${Platform.OS} ${Platform.Version}`)
20
+ import { checkConnectorUrl, handleOAuthUrl } from '@/utils'
21
+ import type { PreFlightCheck } from '@/utils'
24
22
 
25
23
  type QuilttConnectorProps = {
24
+ testId?: string
26
25
  connectorId: string
27
26
  connectionId?: string
28
27
  institution?: string
29
28
  oauthRedirectUrl: string
30
29
  } & ConnectorSDKCallbacks
31
30
 
32
- type PreFlightCheck = {
33
- checked: boolean
34
- error?: string
35
- }
36
-
37
- const PREFLIGHT_RETRY_COUNT = 3
38
-
39
31
  const QuilttConnector = ({
32
+ testId,
40
33
  connectorId,
41
34
  connectionId,
42
35
  institution,
@@ -63,52 +56,14 @@ const QuilttConnector = ({
63
56
  }, [connectorId, encodedOAuthRedirectUrl])
64
57
  const [preFlightCheck, setPreFlightCheck] = useState<PreFlightCheck>({ checked: false })
65
58
 
66
- const checkConnectorUrl = useCallback(
67
- async (retryCount = 0): Promise<PreFlightCheck> => {
68
- let responseStatus
69
- let error
70
- let errorOccurred = false
71
- try {
72
- const response = await fetch(connectorUrl)
73
- if (!response.ok) {
74
- console.error(`The URL ${connectorUrl} is not routable.`)
75
- responseStatus = response.status
76
- errorOccurred = true
77
- } else {
78
- console.log(`The URL ${connectorUrl} is routable.`)
79
- return { checked: true }
80
- }
81
- } catch (e) {
82
- error = e
83
- console.error(`An error occurred while checking the connector URL: ${error}`)
84
- errorOccurred = true
85
- }
86
-
87
- // Retry logic in case of error or response not OK
88
- if (errorOccurred && retryCount < PREFLIGHT_RETRY_COUNT) {
89
- const delay = 50 * Math.pow(2, retryCount) // Exponential back-off
90
- await new Promise((resolve) => setTimeout(resolve, delay)) // delay with exponential back-off for each retry
91
- console.log(`Retrying... Attempt number ${retryCount + 1}`)
92
- return checkConnectorUrl(retryCount + 1)
93
- }
94
-
95
- const errorMessage = getErrorMessage(responseStatus, error as Error)
96
- const errorToSend = (error as Error) || new Error(errorMessage)
97
- const context = { connectorUrl, responseStatus }
98
- if (responseStatus !== 404) errorReporter.send(errorToSend, context)
99
- return { checked: true, error: errorMessage }
100
- },
101
- [connectorUrl]
102
- )
103
-
104
59
  useEffect(() => {
105
60
  if (preFlightCheck.checked) return
106
61
  const fetchDataAndSetState = async () => {
107
- const connectorUrlStatus = await checkConnectorUrl()
62
+ const connectorUrlStatus = await checkConnectorUrl(connectorUrl)
108
63
  setPreFlightCheck(connectorUrlStatus)
109
64
  }
110
65
  fetchDataAndSetState()
111
- }, [checkConnectorUrl, preFlightCheck])
66
+ }, [connectorUrl, preFlightCheck])
112
67
 
113
68
  const initInjectedJavaScript = useCallback(() => {
114
69
  const script = `\
@@ -133,7 +88,7 @@ const QuilttConnector = ({
133
88
 
134
89
  // allowedListUrl & shouldRender ensure we are only rendering Quiltt, MX and Plaid content in Webview
135
90
  // For other urls, we assume those are bank urls, which needs to be handle in external browser.
136
- // @todo Convert it to a list from Quiltt Server to prevent MX/ Plaid changes.
91
+ // TODO: Convert it to a list from Quiltt Server to prevent MX/ Plaid changes.
137
92
  const allowedListUrl = useMemo(
138
93
  () => [
139
94
  'quiltt.app',
@@ -162,14 +117,6 @@ const QuilttConnector = ({
162
117
  webViewRef.current?.injectJavaScript(script)
163
118
  }
164
119
 
165
- const handleOAuthUrl = useCallback((oauthUrl: URL) => {
166
- if (oauthUrl.protocol !== 'https:') {
167
- console.log(`handleOAuthUrl - Skipping non https url - ${oauthUrl.href}`)
168
- return
169
- }
170
- Linking.openURL(oauthUrl.href)
171
- }, [])
172
-
173
120
  const handleQuilttEvent = useCallback(
174
121
  (url: URL) => {
175
122
  url.searchParams.delete('source')
@@ -202,7 +149,7 @@ const QuilttConnector = ({
202
149
  onExitSuccess?.(metadata)
203
150
  break
204
151
  case 'Authenticate':
205
- // @todo handle Authenticate
152
+ // TODO: handle Authenticate
206
153
  break
207
154
  case 'OauthRequested':
208
155
  handleOAuthUrl(new URL(url.searchParams.get('oauthUrl') as string))
@@ -214,7 +161,6 @@ const QuilttConnector = ({
214
161
  },
215
162
  [
216
163
  connectorId,
217
- handleOAuthUrl,
218
164
  initInjectedJavaScript,
219
165
  onEvent,
220
166
  onExit,
@@ -239,18 +185,25 @@ const QuilttConnector = ({
239
185
  handleOAuthUrl(url)
240
186
  return false
241
187
  },
242
- [handleOAuthUrl, handleQuilttEvent, isQuilttEvent, shouldRender]
188
+ [handleQuilttEvent, isQuilttEvent, shouldRender]
243
189
  )
244
190
 
245
- if (!preFlightCheck.checked) return <LoadingScreen />
191
+ if (!preFlightCheck.checked) return <LoadingScreen testId="loading-screen" />
246
192
  if (preFlightCheck.error)
247
- return <ErrorScreen error={preFlightCheck.error} cta={() => onExitError?.({ connectorId })} />
193
+ return (
194
+ <ErrorScreen
195
+ testId="error-screen"
196
+ error={preFlightCheck.error}
197
+ cta={() => onExitError?.({ connectorId })}
198
+ />
199
+ )
248
200
 
249
201
  return (
250
- <AndroidSafeAreaView>
202
+ <AndroidSafeAreaView testId={testId}>
251
203
  <WebView
204
+ testID="webview"
252
205
  ref={webViewRef}
253
- // Plaid keep sending window.location = 'about:srcdoc' and causes some noise in RN
206
+ // Plaid keeps sending window.location = 'about:srcdoc' and causes some noise in RN
254
207
  // All whitelists are now handled in requestHandler, handleQuilttEvent and handleOAuthUrl
255
208
  originWhitelist={['*']}
256
209
  source={{ uri: connectorUrl }}
@@ -0,0 +1,47 @@
1
+ import { getErrorMessage, ErrorReporter } from '../error'
2
+ import { Platform } from 'react-native'
3
+
4
+ const errorReporter = new ErrorReporter(`${Platform.OS} ${Platform.Version}`)
5
+ const PREFLIGHT_RETRY_COUNT = 3
6
+
7
+ export type PreFlightCheck = {
8
+ checked: boolean
9
+ error?: string
10
+ }
11
+
12
+ export const checkConnectorUrl = async (
13
+ connectorUrl: string,
14
+ retryCount = 0
15
+ ): Promise<PreFlightCheck> => {
16
+ let responseStatus
17
+ let error
18
+ let errorOccurred = false
19
+ try {
20
+ const response = await fetch(connectorUrl)
21
+ if (!response.ok) {
22
+ console.error(`The URL ${connectorUrl} is not routable.`)
23
+ responseStatus = response.status
24
+ errorOccurred = true
25
+ } else {
26
+ console.log(`The URL ${connectorUrl} is routable.`)
27
+ return { checked: true }
28
+ }
29
+ } catch (e) {
30
+ error = e
31
+ console.error(`An error occurred while checking the connector URL: ${error}`)
32
+ errorOccurred = true
33
+ }
34
+
35
+ if (errorOccurred && retryCount < PREFLIGHT_RETRY_COUNT) {
36
+ const delay = 50 * Math.pow(2, retryCount)
37
+ await new Promise((resolve) => setTimeout(resolve, delay))
38
+ console.log(`Retrying... Attempt number ${retryCount + 1}`)
39
+ return checkConnectorUrl(connectorUrl, retryCount + 1)
40
+ }
41
+
42
+ const errorMessage = getErrorMessage(responseStatus, error as Error)
43
+ const errorToSend = (error as Error) || new Error(errorMessage)
44
+ const context = { connectorUrl, responseStatus }
45
+ if (responseStatus !== 404) await errorReporter.send(errorToSend, context)
46
+ return { checked: true, error: errorMessage }
47
+ }
@@ -0,0 +1,9 @@
1
+ import { Linking } from 'react-native'
2
+
3
+ export const handleOAuthUrl = (oauthUrl: URL) => {
4
+ if (oauthUrl.protocol !== 'https:') {
5
+ console.log(`handleOAuthUrl - Skipping non https url - ${oauthUrl.href}`)
6
+ return
7
+ }
8
+ Linking.openURL(oauthUrl.href)
9
+ }
File without changes
@@ -0,0 +1,2 @@
1
+ export * from './checkConnectorUrl'
2
+ export * from './handleOAuthUrl'
@@ -1,10 +1,9 @@
1
1
  // Quick hack to send error to Honeybadger to debug why the connector is not routable
2
-
3
2
  import type { Notice, NoticeTransportPayload } from '@honeybadger-io/core/build/src/types'
4
3
  import { generateStackTrace, getCauses, makeBacktrace } from '@honeybadger-io/core/build/src/util'
5
4
 
6
5
  import { ErrorReporterConfig } from './ErrorReporterConfig'
7
- import { version } from '../version'
6
+ import { version } from '../../version'
8
7
 
9
8
  const notifier = {
10
9
  name: 'Quiltt React Native SDK Reporter',
@@ -1,4 +1,3 @@
1
-
2
1
  export const ErrorReporterConfig = {
3
2
  honeybadger_api_key: 'undefined',
4
3
  }
@@ -1,9 +1,7 @@
1
- const getErrorMessage = (responseStatus?: number, error?: Error): string => {
1
+ export const getErrorMessage = (responseStatus?: number, error?: Error): string => {
2
2
  if (error)
3
3
  return `An error occurred while checking the connector URL: ${error?.name} \n${error?.message}`
4
4
  return responseStatus
5
5
  ? `The URL is not routable. Response status: ${responseStatus}`
6
6
  : 'An error occurred while checking the connector URL'
7
7
  }
8
-
9
- export { getErrorMessage }
@@ -0,0 +1,2 @@
1
+ export * from './ErrorReporter'
2
+ export * from './getErrorMessage'
@@ -1,2 +1,2 @@
1
- export * from './ErrorReporter'
2
- export * from './getErrorMessage'
1
+ export * from './connector'
2
+ export * from './error'
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '3.6.3'
2
+ export const version = '3.6.4'