@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 +20 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +118 -10
- package/package.json +6 -6
- package/src/components/QuilttConnector.tsx +90 -14
- package/src/utils/error/ErrorReporter.ts +1 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/url.ts +89 -0
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
|
-
|
|
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.
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
371
|
-
|
|
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.
|
|
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/
|
|
34
|
-
"@quiltt/
|
|
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
|
|
40
|
+
"@types/node": "22.13.10",
|
|
41
41
|
"@types/react": "18.3.12",
|
|
42
42
|
"base-64": "1.0.0",
|
|
43
|
-
"bunchee": "6.
|
|
43
|
+
"bunchee": "6.3.4",
|
|
44
44
|
"react": "18.3.1",
|
|
45
|
-
"react-native": "0.76.
|
|
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 {
|
|
12
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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 '
|
|
5
|
+
import { version } from '@/version'
|
|
6
6
|
import { ErrorReporterConfig } from './ErrorReporterConfig'
|
|
7
7
|
|
|
8
8
|
const notifier = {
|
package/src/utils/index.ts
CHANGED
package/src/utils/url.ts
ADDED
|
@@ -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
|
+
}
|