@quiltt/react-native 3.9.4 → 3.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @quiltt/react-native
2
2
 
3
+ ## 3.9.6
4
+
5
+ ### Patch Changes
6
+
7
+ - [#328](https://github.com/quiltt/quiltt-js/pull/328) [`6b8751c`](https://github.com/quiltt/quiltt-js/commit/6b8751c981e9e74b347227bc9f427585d21870cd) Thanks [@zubairaziz](https://github.com/zubairaziz)! - Updated QuilttConnector OAuth handler
8
+
9
+ - Updated dependencies [[`6b8751c`](https://github.com/quiltt/quiltt-js/commit/6b8751c981e9e74b347227bc9f427585d21870cd)]:
10
+ - @quiltt/react@3.9.6
11
+ - @quiltt/core@3.9.6
12
+
13
+ ## 3.9.5
14
+
15
+ ### Patch Changes
16
+
17
+ - [#325](https://github.com/quiltt/quiltt-js/pull/325) [`62b7323`](https://github.com/quiltt/quiltt-js/commit/62b732371a8d57242170e0ae838baa4ca8e78059) Thanks [@zubairaziz](https://github.com/zubairaziz)! - Improve useSession and useStorage hooks
18
+
19
+ - Updated dependencies [[`62b7323`](https://github.com/quiltt/quiltt-js/commit/62b732371a8d57242170e0ae838baa4ca8e78059)]:
20
+ - @quiltt/react@3.9.5
21
+ - @quiltt/core@3.9.5
22
+
3
23
  ## 3.9.4
4
24
 
5
25
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -9,7 +9,10 @@ type PreFlightCheck = {
9
9
  error?: string;
10
10
  };
11
11
  declare const checkConnectorUrl: (connectorUrl: string, retryCount?: number) => Promise<PreFlightCheck>;
12
- declare const handleOAuthUrl: (oauthUrl: URL | string) => void;
12
+ /**
13
+ * Handle opening OAuth URLs with proper encoding detection and normalization
14
+ */
15
+ declare const handleOAuthUrl: (oauthUrl: URL | string | null | undefined) => void;
13
16
  type QuilttConnectorProps = {
14
17
  testId?: string;
15
18
  connectorId: string;
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ 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 = "3.9.4";
12
+ var version = "3.9.6";
13
13
 
14
14
  const ErrorReporterConfig = {
15
15
  honeybadger_api_key: 'undefined'
@@ -99,6 +99,52 @@ const getErrorMessage = (responseStatus, error)=>{
99
99
  return responseStatus ? `The URL is not routable. Response status: ${responseStatus}` : 'An error occurred while checking the connector URL';
100
100
  };
101
101
 
102
+ /**
103
+ * Checks if a string appears to be already URL encoded
104
+ * @param str The string to check
105
+ * @returns boolean indicating if the string appears to be URL encoded
106
+ */ const isAlreadyEncoded = (str)=>{
107
+ // Check for typical URL encoding patterns like %20, %3A, etc.
108
+ const hasEncodedChars = /%[0-9A-F]{2}/i.test(str);
109
+ // Check if double encoding has occurred (e.g., %253A instead of %3A)
110
+ const hasDoubleEncoding = /%25[0-9A-F]{2}/i.test(str);
111
+ // If we have encoded chars but no double encoding, it's likely properly encoded
112
+ return hasEncodedChars && !hasDoubleEncoding;
113
+ };
114
+ /**
115
+ * Smart URL encoder that ensures a string is encoded exactly once
116
+ * @param str The string to encode
117
+ * @returns A properly URL encoded string
118
+ */ const smartEncodeURIComponent = (str)=>{
119
+ if (!str) return str;
120
+ // If it's already encoded, return as is
121
+ if (isAlreadyEncoded(str)) {
122
+ console.log('URL already encoded, skipping encoding:', str);
123
+ return str;
124
+ }
125
+ // Otherwise, encode it
126
+ const encoded = encodeURIComponent(str);
127
+ console.log('URL encoded from:', str, 'to:', encoded);
128
+ return encoded;
129
+ };
130
+ /**
131
+ * Checks if a string appears to be double-encoded
132
+ */ const isDoubleEncoded = (str)=>{
133
+ if (!str) return false;
134
+ return /%25[0-9A-F]{2}/i.test(str);
135
+ };
136
+ /**
137
+ * Normalizes a URL string by decoding it once if it appears to be double-encoded
138
+ */ const normalizeUrlEncoding = (urlStr)=>{
139
+ if (isDoubleEncoded(urlStr)) {
140
+ console.log('Detected double-encoded URL:', urlStr);
141
+ const normalized = decodeURIComponent(urlStr);
142
+ console.log('Normalized to:', normalized);
143
+ return normalized;
144
+ }
145
+ return urlStr;
146
+ };
147
+
102
148
  const AndroidSafeAreaView = ({ testId, children })=>/*#__PURE__*/ jsx(SafeAreaView, {
103
149
  testID: testId,
104
150
  style: styles$2.AndroidSafeArea,
@@ -249,9 +295,39 @@ const checkConnectorUrl = async (connectorUrl, retryCount = 0)=>{
249
295
  };
250
296
  }
251
297
  };
252
- const handleOAuthUrl = (oauthUrl)=>{
253
- console.log(`handleOAuthUrl - Opening URL - ${oauthUrl.toString()}`);
254
- Linking.openURL(oauthUrl.toString());
298
+ /**
299
+ * Handle opening OAuth URLs with proper encoding detection and normalization
300
+ */ const handleOAuthUrl = (oauthUrl)=>{
301
+ try {
302
+ // Throw error if oauthUrl is null or undefined
303
+ if (oauthUrl == null) {
304
+ throw new Error('handleOAuthUrl - Received null or undefined URL');
305
+ }
306
+ // Convert to string if it's a URL object
307
+ const urlString = oauthUrl.toString();
308
+ // Throw error if the resulting string is empty
309
+ if (!urlString || urlString.trim() === '') {
310
+ throw new Error('handleOAuthUrl - Received empty URL string');
311
+ }
312
+ // Normalize the URL encoding
313
+ const normalizedUrl = normalizeUrlEncoding(urlString);
314
+ // Log the URL we're about to open
315
+ console.log(`handleOAuthUrl - Opening URL - ${normalizedUrl}`);
316
+ // Open the normalized URL
317
+ Linking.openURL(normalizedUrl);
318
+ } catch (error) {
319
+ console.error('Error handling OAuth URL:', error);
320
+ // Only try the fallback if oauthUrl is not null
321
+ if (oauthUrl != null) {
322
+ try {
323
+ const fallbackUrl = typeof oauthUrl === 'string' ? oauthUrl : oauthUrl.toString();
324
+ console.log(`handleOAuthUrl - Fallback opening URL - ${fallbackUrl}`);
325
+ Linking.openURL(fallbackUrl);
326
+ } catch (fallbackError) {
327
+ console.error('Failed even with fallback approach:', fallbackError);
328
+ }
329
+ }
330
+ }
255
331
  };
256
332
  const QuilttConnector = ({ testId, connectorId, connectionId, institution, oauthRedirectUrl, onEvent, onLoad, onExit, onExitSuccess, onExitAbort, onExitError })=>{
257
333
  const webViewRef = useRef(null);
@@ -277,18 +353,35 @@ const QuilttConnector = ({ testId, connectorId, connectionId, institution, oauth
277
353
  webViewRef.current?.injectJavaScript(disableHeaderScrollScript);
278
354
  }
279
355
  }, []);
280
- const encodedOAuthRedirectUrl = useMemo(()=>encodeURIComponent(oauthRedirectUrl), [
356
+ // Ensure oauthRedirectUrl is encoded properly - only once
357
+ const safeOAuthRedirectUrl = useMemo(()=>{
358
+ console.log('Original oauthRedirectUrl:', oauthRedirectUrl);
359
+ return smartEncodeURIComponent(oauthRedirectUrl);
360
+ }, [
281
361
  oauthRedirectUrl
282
362
  ]);
283
363
  const connectorUrl = useMemo(()=>{
284
364
  const url = new URL(`https://${connectorId}.quiltt.app`);
365
+ // For normal parameters, just append them directly
285
366
  url.searchParams.append('mode', 'webview');
286
- url.searchParams.append('oauth_redirect_url', encodedOAuthRedirectUrl);
287
367
  url.searchParams.append('agent', `react-native-${version}`);
288
- return url.toString();
368
+ // For the oauth_redirect_url, we need to be careful
369
+ // If it's already encoded, we need to decode it once to prevent
370
+ // the automatic encoding that happens with searchParams.append
371
+ if (isAlreadyEncoded(safeOAuthRedirectUrl)) {
372
+ const decodedOnce = decodeURIComponent(safeOAuthRedirectUrl);
373
+ url.searchParams.append('oauth_redirect_url', decodedOnce);
374
+ console.log('Using decoded oauth_redirect_url:', decodedOnce);
375
+ } else {
376
+ url.searchParams.append('oauth_redirect_url', safeOAuthRedirectUrl);
377
+ console.log('Using original oauth_redirect_url:', safeOAuthRedirectUrl);
378
+ }
379
+ const finalUrl = url.toString();
380
+ console.log('Final connectorUrl:', finalUrl);
381
+ return finalUrl;
289
382
  }, [
290
383
  connectorId,
291
- encodedOAuthRedirectUrl
384
+ safeOAuthRedirectUrl
292
385
  ]);
293
386
  useEffect(()=>{
294
387
  if (preFlightCheck.checked) return;
@@ -367,8 +460,23 @@ const QuilttConnector = ({ testId, connectorId, connectionId, institution, oauth
367
460
  case 'Authenticate':
368
461
  break;
369
462
  case 'OauthRequested':
370
- handleOAuthUrl(new URL(url.searchParams.get('oauthUrl')));
371
- break;
463
+ {
464
+ // Log available search parameters
465
+ console.log('Available search params:', Array.from(url.searchParams.keys()));
466
+ // Now we should be getting the oauthUrl parameter directly
467
+ const oauthUrl = url.searchParams.get('oauthUrl');
468
+ console.log('Received oauthUrl:', oauthUrl);
469
+ // Check if oauthUrl exists before proceeding
470
+ if (oauthUrl) {
471
+ // Create a new URL from the normalized oauthUrl
472
+ handleOAuthUrl(oauthUrl);
473
+ } else {
474
+ // Log an error if oauthUrl is missing
475
+ console.error('OauthRequested event missing oauthUrl parameter');
476
+ console.log('All available params:', Object.fromEntries(url.searchParams.entries()));
477
+ }
478
+ break;
479
+ }
372
480
  default:
373
481
  console.log('unhandled event', url);
374
482
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quiltt/react-native",
3
- "version": "3.9.4",
3
+ "version": "3.9.6",
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,19 +30,19 @@
30
30
  "dependencies": {
31
31
  "@honeybadger-io/core": "6.6.0",
32
32
  "lodash.debounce": "4.0.8",
33
- "@quiltt/core": "3.9.4",
34
- "@quiltt/react": "3.9.4"
33
+ "@quiltt/react": "3.9.6",
34
+ "@quiltt/core": "3.9.6"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@biomejs/biome": "1.9.4",
38
38
  "@types/base-64": "1.0.2",
39
39
  "@types/lodash.debounce": "4.0.9",
40
- "@types/node": "22.10.5",
40
+ "@types/node": "22.13.10",
41
41
  "@types/react": "18.3.12",
42
42
  "base-64": "1.0.0",
43
- "bunchee": "6.2.0",
43
+ "bunchee": "6.3.4",
44
44
  "react": "18.3.1",
45
- "react-native": "0.76.5",
45
+ "react-native": "0.76.7",
46
46
  "react-native-url-polyfill": "2.0.0",
47
47
  "react-native-webview": "13.12.5",
48
48
  "rimraf": "6.0.1",
@@ -8,8 +8,14 @@ import type { ShouldStartLoadRequest } from 'react-native-webview/lib/WebViewTyp
8
8
  import { ConnectorSDKEventType, useQuilttSession } from '@quiltt/react'
9
9
  import type { ConnectorSDKCallbackMetadata, ConnectorSDKCallbacks } from '@quiltt/react'
10
10
 
11
- import { ErrorReporter, getErrorMessage } from '../utils'
12
- import { version } from '../version'
11
+ import {
12
+ ErrorReporter,
13
+ getErrorMessage,
14
+ isAlreadyEncoded,
15
+ normalizeUrlEncoding,
16
+ smartEncodeURIComponent,
17
+ } from '@/utils'
18
+ import { version } from '@/version'
13
19
  import { AndroidSafeAreaView } from './AndroidSafeAreaView'
14
20
  import { ErrorScreen } from './ErrorScreen'
15
21
  import { LoadingScreen } from './LoadingScreen'
@@ -54,9 +60,46 @@ export const checkConnectorUrl = async (
54
60
  }
55
61
  }
56
62
 
57
- export const handleOAuthUrl = (oauthUrl: URL | string) => {
58
- console.log(`handleOAuthUrl - Opening URL - ${oauthUrl.toString()}`)
59
- Linking.openURL(oauthUrl.toString())
63
+ /**
64
+ * Handle opening OAuth URLs with proper encoding detection and normalization
65
+ */
66
+ export const handleOAuthUrl = (oauthUrl: URL | string | null | undefined) => {
67
+ try {
68
+ // Throw error if oauthUrl is null or undefined
69
+ if (oauthUrl == null) {
70
+ throw new Error('handleOAuthUrl - Received null or undefined URL')
71
+ }
72
+
73
+ // Convert to string if it's a URL object
74
+ const urlString = oauthUrl.toString()
75
+
76
+ // Throw error if the resulting string is empty
77
+ if (!urlString || urlString.trim() === '') {
78
+ throw new Error('handleOAuthUrl - Received empty URL string')
79
+ }
80
+
81
+ // Normalize the URL encoding
82
+ const normalizedUrl = normalizeUrlEncoding(urlString)
83
+
84
+ // Log the URL we're about to open
85
+ console.log(`handleOAuthUrl - Opening URL - ${normalizedUrl}`)
86
+
87
+ // Open the normalized URL
88
+ Linking.openURL(normalizedUrl)
89
+ } catch (error) {
90
+ console.error('Error handling OAuth URL:', error)
91
+
92
+ // Only try the fallback if oauthUrl is not null
93
+ if (oauthUrl != null) {
94
+ try {
95
+ const fallbackUrl = typeof oauthUrl === 'string' ? oauthUrl : oauthUrl.toString()
96
+ console.log(`handleOAuthUrl - Fallback opening URL - ${fallbackUrl}`)
97
+ Linking.openURL(fallbackUrl)
98
+ } catch (fallbackError) {
99
+ console.error('Failed even with fallback approach:', fallbackError)
100
+ }
101
+ }
102
+ }
60
103
  }
61
104
 
62
105
  type QuilttConnectorProps = {
@@ -104,18 +147,35 @@ const QuilttConnector = ({
104
147
  }
105
148
  }, [])
106
149
 
107
- const encodedOAuthRedirectUrl = useMemo(
108
- () => encodeURIComponent(oauthRedirectUrl),
109
- [oauthRedirectUrl]
110
- )
150
+ // Ensure oauthRedirectUrl is encoded properly - only once
151
+ const safeOAuthRedirectUrl = useMemo(() => {
152
+ console.log('Original oauthRedirectUrl:', oauthRedirectUrl)
153
+ return smartEncodeURIComponent(oauthRedirectUrl)
154
+ }, [oauthRedirectUrl])
111
155
 
112
156
  const connectorUrl = useMemo(() => {
113
157
  const url = new URL(`https://${connectorId}.quiltt.app`)
158
+
159
+ // For normal parameters, just append them directly
114
160
  url.searchParams.append('mode', 'webview')
115
- url.searchParams.append('oauth_redirect_url', encodedOAuthRedirectUrl)
116
161
  url.searchParams.append('agent', `react-native-${version}`)
117
- return url.toString()
118
- }, [connectorId, encodedOAuthRedirectUrl])
162
+
163
+ // For the oauth_redirect_url, we need to be careful
164
+ // If it's already encoded, we need to decode it once to prevent
165
+ // the automatic encoding that happens with searchParams.append
166
+ if (isAlreadyEncoded(safeOAuthRedirectUrl)) {
167
+ const decodedOnce = decodeURIComponent(safeOAuthRedirectUrl)
168
+ url.searchParams.append('oauth_redirect_url', decodedOnce)
169
+ console.log('Using decoded oauth_redirect_url:', decodedOnce)
170
+ } else {
171
+ url.searchParams.append('oauth_redirect_url', safeOAuthRedirectUrl)
172
+ console.log('Using original oauth_redirect_url:', safeOAuthRedirectUrl)
173
+ }
174
+
175
+ const finalUrl = url.toString()
176
+ console.log('Final connectorUrl:', finalUrl)
177
+ return finalUrl
178
+ }, [connectorId, safeOAuthRedirectUrl])
119
179
 
120
180
  useEffect(() => {
121
181
  if (preFlightCheck.checked) return
@@ -191,9 +251,25 @@ const QuilttConnector = ({
191
251
  case 'Authenticate':
192
252
  // TODO: handle Authenticate
193
253
  break
194
- case 'OauthRequested':
195
- handleOAuthUrl(new URL(url.searchParams.get('oauthUrl') as string))
254
+ case 'OauthRequested': {
255
+ // Log available search parameters
256
+ console.log('Available search params:', Array.from(url.searchParams.keys()))
257
+
258
+ // Now we should be getting the oauthUrl parameter directly
259
+ const oauthUrl = url.searchParams.get('oauthUrl')
260
+ console.log('Received oauthUrl:', oauthUrl)
261
+
262
+ // Check if oauthUrl exists before proceeding
263
+ if (oauthUrl) {
264
+ // Create a new URL from the normalized oauthUrl
265
+ handleOAuthUrl(oauthUrl)
266
+ } else {
267
+ // Log an error if oauthUrl is missing
268
+ console.error('OauthRequested event missing oauthUrl parameter')
269
+ console.log('All available params:', Object.fromEntries(url.searchParams.entries()))
270
+ }
196
271
  break
272
+ }
197
273
  default:
198
274
  console.log('unhandled event', url)
199
275
  break
@@ -2,7 +2,7 @@
2
2
  import type { Notice, NoticeTransportPayload } from '@honeybadger-io/core/build/src/types'
3
3
  import { generateStackTrace, getCauses, makeBacktrace } from '@honeybadger-io/core/build/src/util'
4
4
 
5
- import { version } from '../../version'
5
+ import { version } from '@/version'
6
6
  import { ErrorReporterConfig } from './ErrorReporterConfig'
7
7
 
8
8
  const notifier = {
@@ -1 +1,2 @@
1
1
  export * from './error'
2
+ export * from './url'
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Checks if a string appears to be already URL encoded
3
+ * @param str The string to check
4
+ * @returns boolean indicating if the string appears to be URL encoded
5
+ */
6
+ export const isAlreadyEncoded = (str: string): boolean => {
7
+ // Check for typical URL encoding patterns like %20, %3A, etc.
8
+ const hasEncodedChars = /%[0-9A-F]{2}/i.test(str)
9
+
10
+ // Check if double encoding has occurred (e.g., %253A instead of %3A)
11
+ const hasDoubleEncoding = /%25[0-9A-F]{2}/i.test(str)
12
+
13
+ // If we have encoded chars but no double encoding, it's likely properly encoded
14
+ return hasEncodedChars && !hasDoubleEncoding
15
+ }
16
+
17
+ /**
18
+ * Smart URL encoder that ensures a string is encoded exactly once
19
+ * @param str The string to encode
20
+ * @returns A properly URL encoded string
21
+ */
22
+ export const smartEncodeURIComponent = (str: string): string => {
23
+ if (!str) return str
24
+
25
+ // If it's already encoded, return as is
26
+ if (isAlreadyEncoded(str)) {
27
+ console.log('URL already encoded, skipping encoding:', str)
28
+ return str
29
+ }
30
+
31
+ // Otherwise, encode it
32
+ const encoded = encodeURIComponent(str)
33
+ console.log('URL encoded from:', str, 'to:', encoded)
34
+ return encoded
35
+ }
36
+
37
+ /**
38
+ * Creates a URL with proper parameter encoding
39
+ * @param baseUrl The base URL string
40
+ * @param params Object containing key-value pairs to be added as search params
41
+ * @returns A properly formatted URL string
42
+ */
43
+ export const createUrlWithParams = (baseUrl: string, params: Record<string, string>): string => {
44
+ try {
45
+ const url = new URL(baseUrl)
46
+
47
+ // Add each parameter without additional encoding
48
+ // (URLSearchParams.append will encode them automatically)
49
+ Object.entries(params).forEach(([key, value]) => {
50
+ // Skip undefined or null values
51
+ if (value == null) return
52
+
53
+ // For oauth_redirect_url specifically, ensure it's not double encoded
54
+ if (key === 'oauth_redirect_url' && isAlreadyEncoded(value)) {
55
+ // Decode once to counteract the automatic encoding that will happen
56
+ const decodedOnce = decodeURIComponent(value)
57
+ url.searchParams.append(key, decodedOnce)
58
+ } else {
59
+ url.searchParams.append(key, value)
60
+ }
61
+ })
62
+
63
+ return url.toString()
64
+ } catch (error) {
65
+ console.error('Error creating URL with params:', error)
66
+ return baseUrl
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Checks if a string appears to be double-encoded
72
+ */
73
+ const isDoubleEncoded = (str: string): boolean => {
74
+ if (!str) return false
75
+ return /%25[0-9A-F]{2}/i.test(str)
76
+ }
77
+
78
+ /**
79
+ * Normalizes a URL string by decoding it once if it appears to be double-encoded
80
+ */
81
+ export const normalizeUrlEncoding = (urlStr: string): string => {
82
+ if (isDoubleEncoded(urlStr)) {
83
+ console.log('Detected double-encoded URL:', urlStr)
84
+ const normalized = decodeURIComponent(urlStr)
85
+ console.log('Normalized to:', normalized)
86
+ return normalized
87
+ }
88
+ return urlStr
89
+ }