@quiltt/react-native 3.6.3 → 3.6.5
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 +20 -0
- package/dist/index.cjs +158 -154
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +447 -0
- package/package.json +9 -7
- package/src/components/AndroidSafeAreaView.tsx +8 -2
- package/src/components/ErrorScreen.tsx +4 -3
- package/src/components/LoadingScreen.tsx +7 -3
- package/src/components/QuilttConnector.tsx +22 -74
- package/src/utils/connector/checkConnectorUrl.ts +47 -0
- package/src/utils/connector/handleOAuthUrl.ts +9 -0
- package/src/utils/connector/handleQuilttEvent.ts +0 -0
- package/src/utils/connector/index.ts +2 -0
- package/src/utils/{ErrorReporter.ts → error/ErrorReporter.ts} +1 -2
- package/src/utils/{ErrorReporterConfig.ts → error/ErrorReporterConfig.ts} +0 -1
- package/src/utils/{getErrorMessage.ts → error/getErrorMessage.ts} +1 -3
- package/src/utils/error/index.ts +2 -0
- package/src/utils/index.ts +2 -2
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @quiltt/react-native
|
|
2
2
|
|
|
3
|
+
## 3.6.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#248](https://github.com/quiltt/quiltt-js/pull/248) [`d15297e`](https://github.com/quiltt/quiltt-js/commit/d15297e4dea40c90dab97d1f8e8797b5cfe8395c) Thanks [@sirwolfgang](https://github.com/sirwolfgang)! - Fix issue with loading Plaid's new Link
|
|
8
|
+
|
|
9
|
+
- Updated dependencies [[`d15297e`](https://github.com/quiltt/quiltt-js/commit/d15297e4dea40c90dab97d1f8e8797b5cfe8395c)]:
|
|
10
|
+
- @quiltt/core@3.6.5
|
|
11
|
+
- @quiltt/react@3.6.5
|
|
12
|
+
|
|
13
|
+
## 3.6.4
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [#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
|
|
18
|
+
|
|
19
|
+
- Updated dependencies [[`38f7904`](https://github.com/quiltt/quiltt-js/commit/38f79048e99dc617d700b62fd285623f9f2ae2fa)]:
|
|
20
|
+
- @quiltt/react@3.6.4
|
|
21
|
+
- @quiltt/core@3.6.4
|
|
22
|
+
|
|
3
23
|
## 3.6.3
|
|
4
24
|
|
|
5
25
|
### 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.
|
|
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.5';
|
|
98
15
|
|
|
99
|
-
const
|
|
100
|
-
|
|
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: "#
|
|
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
|
|
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
|
-
|
|
294
|
+
connectorUrl,
|
|
287
295
|
preFlightCheck
|
|
288
296
|
]);
|
|
289
297
|
const initInjectedJavaScript = react$1.useCallback(()=>{
|
|
@@ -313,12 +321,12 @@ 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
|
-
//
|
|
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',
|
|
320
328
|
'moneydesktop.com',
|
|
321
|
-
'cdn.plaid.com
|
|
329
|
+
'cdn.plaid.com'
|
|
322
330
|
], []);
|
|
323
331
|
const isQuilttEvent = react$1.useCallback((url)=>url.protocol === 'quilttconnector:', []);
|
|
324
332
|
const shouldRender = react$1.useCallback((url)=>{
|
|
@@ -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
|
|
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.5';
|
|
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'
|
|
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
|
+
"version": "3.6.5",
|
|
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
|
-
"
|
|
16
|
-
"types": "./dist/index.d.ts"
|
|
18
|
+
"default": "./dist/index.cjs"
|
|
17
19
|
}
|
|
18
20
|
},
|
|
19
|
-
"main": "./dist/index.
|
|
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/
|
|
29
|
-
"@quiltt/
|
|
30
|
+
"@quiltt/core": "3.6.5",
|
|
31
|
+
"@quiltt/react": "3.6.5"
|
|
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.
|
|
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
|
-
|
|
6
|
-
|
|
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
|
|
5
|
+
type ErrorScreenProps = {
|
|
6
|
+
testId?: string
|
|
6
7
|
error: string
|
|
7
8
|
cta: () => void
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
export const ErrorScreen = ({ error, cta }:
|
|
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
|
-
|
|
6
|
-
|
|
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="#
|
|
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 {
|
|
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
|
-
|
|
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
|
-
}, [
|
|
66
|
+
}, [connectorUrl, preFlightCheck])
|
|
112
67
|
|
|
113
68
|
const initInjectedJavaScript = useCallback(() => {
|
|
114
69
|
const script = `\
|
|
@@ -133,14 +88,9 @@ 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
|
-
//
|
|
91
|
+
// TODO: Convert it to a list from Quiltt Server to prevent MX/ Plaid changes.
|
|
137
92
|
const allowedListUrl = useMemo(
|
|
138
|
-
() => [
|
|
139
|
-
'quiltt.app',
|
|
140
|
-
'quiltt.dev',
|
|
141
|
-
'moneydesktop.com',
|
|
142
|
-
'cdn.plaid.com/link/v2/stable/link.html',
|
|
143
|
-
],
|
|
93
|
+
() => ['quiltt.app', 'quiltt.dev', 'moneydesktop.com', 'cdn.plaid.com'],
|
|
144
94
|
[]
|
|
145
95
|
)
|
|
146
96
|
|
|
@@ -162,14 +112,6 @@ const QuilttConnector = ({
|
|
|
162
112
|
webViewRef.current?.injectJavaScript(script)
|
|
163
113
|
}
|
|
164
114
|
|
|
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
115
|
const handleQuilttEvent = useCallback(
|
|
174
116
|
(url: URL) => {
|
|
175
117
|
url.searchParams.delete('source')
|
|
@@ -202,7 +144,7 @@ const QuilttConnector = ({
|
|
|
202
144
|
onExitSuccess?.(metadata)
|
|
203
145
|
break
|
|
204
146
|
case 'Authenticate':
|
|
205
|
-
//
|
|
147
|
+
// TODO: handle Authenticate
|
|
206
148
|
break
|
|
207
149
|
case 'OauthRequested':
|
|
208
150
|
handleOAuthUrl(new URL(url.searchParams.get('oauthUrl') as string))
|
|
@@ -214,7 +156,6 @@ const QuilttConnector = ({
|
|
|
214
156
|
},
|
|
215
157
|
[
|
|
216
158
|
connectorId,
|
|
217
|
-
handleOAuthUrl,
|
|
218
159
|
initInjectedJavaScript,
|
|
219
160
|
onEvent,
|
|
220
161
|
onExit,
|
|
@@ -239,18 +180,25 @@ const QuilttConnector = ({
|
|
|
239
180
|
handleOAuthUrl(url)
|
|
240
181
|
return false
|
|
241
182
|
},
|
|
242
|
-
[
|
|
183
|
+
[handleQuilttEvent, isQuilttEvent, shouldRender]
|
|
243
184
|
)
|
|
244
185
|
|
|
245
|
-
if (!preFlightCheck.checked) return <LoadingScreen />
|
|
186
|
+
if (!preFlightCheck.checked) return <LoadingScreen testId="loading-screen" />
|
|
246
187
|
if (preFlightCheck.error)
|
|
247
|
-
return
|
|
188
|
+
return (
|
|
189
|
+
<ErrorScreen
|
|
190
|
+
testId="error-screen"
|
|
191
|
+
error={preFlightCheck.error}
|
|
192
|
+
cta={() => onExitError?.({ connectorId })}
|
|
193
|
+
/>
|
|
194
|
+
)
|
|
248
195
|
|
|
249
196
|
return (
|
|
250
|
-
<AndroidSafeAreaView>
|
|
197
|
+
<AndroidSafeAreaView testId={testId}>
|
|
251
198
|
<WebView
|
|
199
|
+
testID="webview"
|
|
252
200
|
ref={webViewRef}
|
|
253
|
-
// Plaid
|
|
201
|
+
// Plaid keeps sending window.location = 'about:srcdoc' and causes some noise in RN
|
|
254
202
|
// All whitelists are now handled in requestHandler, handleQuilttEvent and handleOAuthUrl
|
|
255
203
|
originWhitelist={['*']}
|
|
256
204
|
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
|
+
}
|
|
File without changes
|
|
@@ -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 '
|
|
6
|
+
import { version } from '../../version'
|
|
8
7
|
|
|
9
8
|
const notifier = {
|
|
10
9
|
name: 'Quiltt React Native SDK Reporter',
|
|
@@ -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 }
|
package/src/utils/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from './
|
|
2
|
-
export * from './
|
|
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.
|
|
2
|
+
export const version = '3.6.5'
|