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