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