@exodus/react-native-webview 11.26.1-exodus.8 → 13.16.0-exodus.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.
Files changed (118) hide show
  1. package/README.md +36 -63
  2. package/android/build.gradle +83 -110
  3. package/android/gradle.properties +3 -4
  4. package/android/src/main/AndroidManifest.xml +12 -0
  5. package/android/src/main/AndroidManifestNew.xml +26 -0
  6. package/android/src/main/java/com/reactnativecommunity/webview/RNCBasicAuthCredential.java +11 -0
  7. package/android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java +407 -0
  8. package/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java +468 -0
  9. package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewClient.java +330 -0
  10. package/android/src/main/java/com/reactnativecommunity/webview/{WebViewConfig.java → RNCWebViewConfig.java} +3 -4
  11. package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewFileProvider.java +1 -1
  12. package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt +746 -0
  13. package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewMessagingModule.kt +9 -0
  14. package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModuleImpl.java +554 -0
  15. package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewPackage.java +57 -12
  16. package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewWrapper.kt +39 -0
  17. package/android/src/main/java/com/reactnativecommunity/webview/events/SubResourceErrorEvent.kt +25 -0
  18. package/android/src/main/java/com/reactnativecommunity/webview/events/TopCustomMenuSelectionEvent.kt +24 -0
  19. package/android/src/main/java/com/reactnativecommunity/webview/events/TopHttpErrorEvent.kt +25 -0
  20. package/android/src/main/java/com/reactnativecommunity/webview/events/TopNewWindowEvent.kt +25 -0
  21. package/android/src/main/java/com/reactnativecommunity/webview/events/TopRenderProcessGoneEvent.kt +25 -0
  22. package/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java +570 -0
  23. package/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewModule.java +57 -0
  24. package/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java +341 -0
  25. package/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewModule.java +59 -0
  26. package/apple/RCTConvert+WKDataDetectorTypes.h +11 -0
  27. package/apple/RCTConvert+WKDataDetectorTypes.m +27 -0
  28. package/apple/RNCWebView.h +26 -100
  29. package/apple/RNCWebView.mm +555 -0
  30. package/apple/RNCWebViewDecisionManager.h +20 -0
  31. package/apple/RNCWebViewDecisionManager.m +47 -0
  32. package/apple/RNCWebViewImpl.h +164 -0
  33. package/apple/{RNCWebView.m → RNCWebViewImpl.m} +803 -226
  34. package/apple/RNCWebViewManager.h +4 -8
  35. package/apple/RNCWebViewManager.mm +221 -0
  36. package/apple/RNCWebViewModule.h +23 -0
  37. package/apple/RNCWebViewModule.mm +34 -0
  38. package/index.d.ts +2 -3
  39. package/lib/NativeRNCWebViewModule.d.ts +8 -0
  40. package/lib/NativeRNCWebViewModule.js +1 -0
  41. package/lib/RNCWebViewNativeComponent.d.ts +245 -0
  42. package/lib/RNCWebViewNativeComponent.js +1 -0
  43. package/lib/WebView.android.d.ts +0 -1
  44. package/lib/WebView.android.js +1 -135
  45. package/lib/WebView.d.ts +2 -3
  46. package/lib/WebView.ios.d.ts +0 -1
  47. package/lib/WebView.ios.js +1 -114
  48. package/lib/WebView.js +1 -11
  49. package/lib/WebView.macos.d.ts +6 -0
  50. package/lib/WebView.macos.js +1 -0
  51. package/lib/WebView.styles.d.ts +37 -11
  52. package/lib/WebView.styles.js +1 -33
  53. package/lib/WebView.windows.d.ts +17 -0
  54. package/lib/WebView.windows.js +1 -0
  55. package/lib/WebViewNativeComponent.macos.d.ts +3 -0
  56. package/lib/WebViewNativeComponent.macos.js +1 -0
  57. package/lib/WebViewNativeComponent.windows.d.ts +3 -0
  58. package/lib/WebViewNativeComponent.windows.js +1 -0
  59. package/lib/WebViewShared.d.ts +30 -9
  60. package/lib/WebViewShared.js +1 -174
  61. package/lib/WebViewTypes.d.ts +514 -98
  62. package/lib/WebViewTypes.js +1 -6
  63. package/lib/index.d.ts +0 -1
  64. package/lib/index.js +1 -3
  65. package/lib/validation.d.ts +3 -0
  66. package/lib/validation.js +1 -0
  67. package/package.json +57 -33
  68. package/react-native-webview.podspec +32 -5
  69. package/react-native.config.js +22 -18
  70. package/src/NativeRNCWebViewModule.ts +13 -0
  71. package/src/RNCWebViewNativeComponent.ts +348 -0
  72. package/src/WebView.android.tsx +345 -0
  73. package/src/WebView.ios.tsx +341 -0
  74. package/src/WebView.macos.tsx +252 -0
  75. package/src/WebView.styles.ts +41 -0
  76. package/src/WebView.tsx +25 -0
  77. package/src/WebView.windows.tsx +217 -0
  78. package/src/WebViewNativeComponent.macos.ts +7 -0
  79. package/src/WebViewNativeComponent.windows.ts +8 -0
  80. package/src/WebViewShared.tsx +476 -0
  81. package/src/WebViewTypes.ts +1402 -0
  82. package/src/__tests__/WebViewShared-test.js +323 -0
  83. package/src/__tests__/__snapshots__/WebViewShared-test.js.snap +8 -0
  84. package/src/__tests__/validation-test.js +38 -0
  85. package/src/index.ts +4 -0
  86. package/src/validation.ts +20 -0
  87. package/android/.editorconfig +0 -6
  88. package/android/.gradle/7.4.2/checksums/checksums.lock +0 -0
  89. package/android/.gradle/7.4.2/dependencies-accessors/dependencies-accessors.lock +0 -0
  90. package/android/.gradle/7.4.2/dependencies-accessors/gc.properties +0 -0
  91. package/android/.gradle/7.4.2/executionHistory/executionHistory.lock +0 -0
  92. package/android/.gradle/7.4.2/fileChanges/last-build.bin +0 -0
  93. package/android/.gradle/7.4.2/fileHashes/fileHashes.lock +0 -0
  94. package/android/.gradle/7.4.2/gc.properties +0 -0
  95. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  96. package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
  97. package/android/.gradle/vcs-1/gc.properties +0 -0
  98. package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java +0 -1408
  99. package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java +0 -506
  100. package/apple/RNCWebViewManager.m +0 -278
  101. package/ios/Pods/Manifest.lock +0 -3
  102. package/ios/Pods/Pods.xcodeproj/project.pbxproj +0 -397
  103. package/ios/Pods/Pods.xcodeproj/xcuserdata/gabrielezenwankwo.xcuserdatad/xcschemes/Pods-RNCWebView.xcscheme +0 -58
  104. package/ios/Pods/Pods.xcodeproj/xcuserdata/gabrielezenwankwo.xcuserdatad/xcschemes/xcschememanagement.plist +0 -16
  105. package/ios/Pods/Target Support Files/Pods-RNCWebView/Pods-RNCWebView-Info.plist +0 -26
  106. package/ios/Pods/Target Support Files/Pods-RNCWebView/Pods-RNCWebView-acknowledgements.markdown +0 -3
  107. package/ios/Pods/Target Support Files/Pods-RNCWebView/Pods-RNCWebView-acknowledgements.plist +0 -29
  108. package/ios/Pods/Target Support Files/Pods-RNCWebView/Pods-RNCWebView-dummy.m +0 -5
  109. package/ios/Pods/Target Support Files/Pods-RNCWebView/Pods-RNCWebView-umbrella.h +0 -16
  110. package/ios/Pods/Target Support Files/Pods-RNCWebView/Pods-RNCWebView.debug.xcconfig +0 -8
  111. package/ios/Pods/Target Support Files/Pods-RNCWebView/Pods-RNCWebView.modulemap +0 -6
  112. package/ios/Pods/Target Support Files/Pods-RNCWebView/Pods-RNCWebView.release.xcconfig +0 -8
  113. package/lib/UpdateOS.d.ts +0 -6
  114. package/lib/UpdateOS.js +0 -49
  115. package/lib/WebViewNativeComponent.android.d.ts +0 -4
  116. package/lib/WebViewNativeComponent.android.js +0 -3
  117. package/lib/WebViewNativeComponent.ios.d.ts +0 -4
  118. package/lib/WebViewNativeComponent.ios.js +0 -3
@@ -0,0 +1,476 @@
1
+ import escapeStringRegexp from 'escape-string-regexp';
2
+ import React, { useCallback, useMemo, useRef, useState } from 'react';
3
+ import { Linking, View, ActivityIndicator, Text, Platform } from 'react-native';
4
+ import {
5
+ OnShouldStartLoadWithRequest,
6
+ ShouldStartLoadRequestEvent,
7
+ WebViewError,
8
+ WebViewErrorEvent,
9
+ WebViewHttpErrorEvent,
10
+ WebViewMessage,
11
+ WebViewMessageEvent,
12
+ WebViewNavigation,
13
+ WebViewNativeEvent,
14
+ WebViewNavigationEvent,
15
+ WebViewOpenWindowEvent,
16
+ WebViewProgressEvent,
17
+ WebViewRenderProcessGoneEvent,
18
+ WebViewTerminatedEvent,
19
+ } from './WebViewTypes';
20
+ import styles from './WebView.styles';
21
+
22
+ const defaultOriginWhitelist = ['http://*', 'https://*'] as const;
23
+
24
+ // Exodus: Default protocol schemes for deep linking
25
+ const defaultDeeplinkWhitelist = ['https:'] as const;
26
+ const defaultDeeplinkBlocklist = ['http:', 'file:', 'javascript:'] as const;
27
+
28
+ // Exodus: Extract protocol scheme from URL using native URL parsing
29
+ const urlToProtocolScheme = (url: string): string | null => {
30
+ try {
31
+ return new URL(url).protocol;
32
+ } catch {
33
+ // Protocol schemes must start with a letter and cannot start with digits, underscores etc.
34
+ // e.g 0invalid, _invalid, +invalid, -invalid, .invalid will all become null
35
+ return null;
36
+ }
37
+ };
38
+
39
+ // Exodus: Check if a value exists in a list of strings
40
+ const matchWithStringList = (
41
+ prefixes: readonly string[],
42
+ value: string
43
+ ): boolean => {
44
+ if (typeof value !== 'string') {
45
+ throw new Error('value was not a string');
46
+ }
47
+ return Array.prototype.includes.call(prefixes, value);
48
+ };
49
+
50
+ // Exodus: Convert whitelist string to RegExp with exact matching (^ and $ anchors)
51
+ const stringWhitelistToRegex = (originWhitelist: string): RegExp =>
52
+ new RegExp(`^${escapeStringRegexp(originWhitelist).replace(/\\\*/g, '.*')}$`);
53
+
54
+ // Exodus: Test value against a list of compiled RegExp patterns
55
+ const matchWithRegexList = (
56
+ compiledRegexList: readonly RegExp[],
57
+ value: string
58
+ ): boolean => {
59
+ return compiledRegexList.some((x) => x.test(value));
60
+ };
61
+
62
+ // Exodus: Compile whitelist strings into RegExp array for efficient matching
63
+ const compileWhitelist = (
64
+ originWhitelist: readonly string[]
65
+ ): readonly RegExp[] =>
66
+ ['about:blank', ...(originWhitelist || [])].map(stringWhitelistToRegex);
67
+
68
+ // Exodus: Check if URL passes whitelist using native URL API for robust parsing
69
+ // Falls back to href when origin is null (handles data:, blob:, etc.)
70
+ const passesWhitelist = (
71
+ compiledWhitelist: readonly RegExp[],
72
+ url: string
73
+ ): boolean => {
74
+ try {
75
+ const { href, origin } = new URL(url);
76
+
77
+ // Check origin first (most common case)
78
+ if (origin && origin !== 'null') {
79
+ return matchWithRegexList(compiledWhitelist, origin);
80
+ }
81
+
82
+ // Fallback to href for URLs where origin is null (data:, blob:, javascript:, etc.)
83
+ return matchWithRegexList(compiledWhitelist, href);
84
+ } catch {
85
+ // Malformed URL - fail closed for security
86
+ return false;
87
+ }
88
+ };
89
+
90
+ const createOnShouldStartLoadWithRequest = (
91
+ loadRequest: (
92
+ shouldStart: boolean,
93
+ url: string,
94
+ lockIdentifier: number
95
+ ) => void,
96
+ originWhitelist: readonly string[],
97
+ deeplinkWhitelist: readonly string[],
98
+ onShouldStartLoadWithRequest?: OnShouldStartLoadWithRequest
99
+ ) => {
100
+ const compiledWhitelist = compileWhitelist(originWhitelist);
101
+
102
+ return ({ nativeEvent }: ShouldStartLoadRequestEvent) => {
103
+ let shouldStart = true;
104
+ const { url, lockIdentifier, isTopFrame } = nativeEvent;
105
+
106
+ // Exodus: Check if the url passes the origin whitelist
107
+ if (!passesWhitelist(compiledWhitelist, url)) {
108
+ const protocol = urlToProtocolScheme(url);
109
+
110
+ // Check that the protocol was properly parsed
111
+ if (protocol !== null) {
112
+ // Exodus: Check if the protocol passes the hardcoded deeplink blocklist
113
+ const foundMatchInBlocklist = matchWithStringList(
114
+ defaultDeeplinkBlocklist,
115
+ protocol
116
+ );
117
+ if (!foundMatchInBlocklist) {
118
+ // Exodus: Check if the protocol passes the dynamic deeplink allowlist
119
+ const foundMatchInAllowlist = matchWithStringList(
120
+ deeplinkWhitelist,
121
+ protocol
122
+ );
123
+
124
+ if (foundMatchInAllowlist) {
125
+ Linking.canOpenURL(url)
126
+ .then((supported) => {
127
+ // Allow mailto: even if canOpenURL returns false (RN Linking quirk)
128
+ if (
129
+ (supported && isTopFrame) ||
130
+ protocol.startsWith('mailto:')
131
+ ) {
132
+ return Linking.openURL(url);
133
+ }
134
+ console.warn(`Can't open url: ${url}`);
135
+ return undefined;
136
+ })
137
+ .catch((e) => {
138
+ console.warn('Error opening URL: ', e);
139
+ });
140
+ } else {
141
+ console.warn(`Failed to pass whitelist for deep link url: ${url}`);
142
+ }
143
+ } else {
144
+ console.warn(
145
+ `Failed to pass default block list for deep link url: ${url}`
146
+ );
147
+ }
148
+ }
149
+
150
+ shouldStart = false;
151
+ } else if (onShouldStartLoadWithRequest) {
152
+ shouldStart = onShouldStartLoadWithRequest(nativeEvent);
153
+ }
154
+
155
+ loadRequest(shouldStart, url, lockIdentifier);
156
+ };
157
+ };
158
+
159
+ const defaultRenderLoading = () => (
160
+ <View style={styles.loadingOrErrorView}>
161
+ <ActivityIndicator />
162
+ </View>
163
+ );
164
+ const defaultRenderError = (
165
+ errorDomain: string | undefined,
166
+ errorCode: number,
167
+ errorDesc: string
168
+ ) => (
169
+ <View style={styles.loadingOrErrorView}>
170
+ <Text style={styles.errorTextTitle}>Error loading page</Text>
171
+ <Text style={styles.errorText}>{`Domain: ${errorDomain}`}</Text>
172
+ <Text style={styles.errorText}>{`Error Code: ${errorCode}`}</Text>
173
+ <Text style={styles.errorText}>{`Description: ${errorDesc}`}</Text>
174
+ </View>
175
+ );
176
+
177
+ export {
178
+ defaultOriginWhitelist,
179
+ defaultDeeplinkWhitelist,
180
+ createOnShouldStartLoadWithRequest,
181
+ defaultRenderLoading,
182
+ defaultRenderError,
183
+ };
184
+
185
+ export const useWebViewLogic = ({
186
+ startInLoadingState,
187
+ onNavigationStateChange,
188
+ onLoadStart,
189
+ onLoad,
190
+ onLoadProgress,
191
+ onLoadEnd,
192
+ onError,
193
+ onLoadSubResourceError,
194
+ onHttpErrorProp,
195
+ onMessageProp,
196
+ onOpenWindowProp,
197
+ onRenderProcessGoneProp,
198
+ onContentProcessDidTerminateProp,
199
+ originWhitelist,
200
+ deeplinkWhitelist,
201
+ onShouldStartLoadWithRequestProp,
202
+ onShouldStartLoadWithRequestCallback,
203
+ validateMeta,
204
+ validateData,
205
+ }: {
206
+ startInLoadingState?: boolean;
207
+ onNavigationStateChange?: (event: WebViewNavigation) => void;
208
+ onLoadStart?: (event: WebViewNavigationEvent) => void;
209
+ onLoad?: (event: WebViewNavigationEvent) => void;
210
+ onLoadProgress?: (event: WebViewProgressEvent) => void;
211
+ onLoadEnd?: (event: WebViewNavigationEvent | WebViewErrorEvent) => void;
212
+ onError?: (event: WebViewErrorEvent) => void;
213
+ onLoadSubResourceError?: (event: WebViewErrorEvent) => void;
214
+ onHttpErrorProp?: (event: WebViewHttpErrorEvent) => void;
215
+ onMessageProp?: (event: WebViewMessage) => void;
216
+ onOpenWindowProp?: (event: WebViewOpenWindowEvent) => void;
217
+ onRenderProcessGoneProp?: (event: WebViewRenderProcessGoneEvent) => void;
218
+ onContentProcessDidTerminateProp?: (event: WebViewTerminatedEvent) => void;
219
+ originWhitelist: readonly string[];
220
+ deeplinkWhitelist: readonly string[];
221
+ onShouldStartLoadWithRequestProp?: OnShouldStartLoadWithRequest;
222
+ onShouldStartLoadWithRequestCallback: (
223
+ shouldStart: boolean,
224
+ url: string,
225
+ lockIdentifier?: number | undefined
226
+ ) => void;
227
+ validateMeta: (event: WebViewNativeEvent) => WebViewNativeEvent;
228
+ validateData: (data: object) => object;
229
+ }) => {
230
+ const [viewState, setViewState] = useState<'IDLE' | 'LOADING' | 'ERROR'>(
231
+ startInLoadingState ? 'LOADING' : 'IDLE'
232
+ );
233
+ const [lastErrorEvent, setLastErrorEvent] = useState<WebViewError | null>(
234
+ null
235
+ );
236
+ const startUrl = useRef<string | null>(null);
237
+
238
+ // Exodus: Helper to check if URL passes origin whitelist
239
+ const passesWhitelistCallback = useCallback(
240
+ (url: string) => {
241
+ if (!url || typeof url !== 'string') return false;
242
+ return passesWhitelist(compileWhitelist(originWhitelist), url);
243
+ },
244
+ [originWhitelist]
245
+ );
246
+
247
+ // Exodus: Extract and sanitize metadata from native event
248
+ const extractMeta = (
249
+ nativeEvent: WebViewNativeEvent
250
+ ): WebViewNativeEvent => ({
251
+ url: String(nativeEvent.url),
252
+ loading: Boolean(nativeEvent.loading),
253
+ title: String(nativeEvent.title).slice(0, 512),
254
+ canGoBack: Boolean(nativeEvent.canGoBack),
255
+ canGoForward: Boolean(nativeEvent.canGoForward),
256
+ lockIdentifier: Number(nativeEvent.lockIdentifier),
257
+ });
258
+
259
+ const updateNavigationState = useCallback(
260
+ (event: WebViewNavigationEvent) => {
261
+ onNavigationStateChange?.(event.nativeEvent);
262
+ },
263
+ [onNavigationStateChange]
264
+ );
265
+
266
+ const onLoadingStart = useCallback(
267
+ (event: WebViewNavigationEvent) => {
268
+ // Needed for android
269
+ startUrl.current = event.nativeEvent.url;
270
+ // !Needed for android
271
+
272
+ onLoadStart?.(event);
273
+ updateNavigationState(event);
274
+ },
275
+ [onLoadStart, updateNavigationState]
276
+ );
277
+
278
+ const onLoadingError = useCallback(
279
+ (event: WebViewErrorEvent) => {
280
+ event.persist();
281
+ if (onError) {
282
+ onError(event);
283
+ } else {
284
+ console.warn('Encountered an error loading page', event.nativeEvent);
285
+ }
286
+ onLoadEnd?.(event);
287
+ if (event.isDefaultPrevented()) {
288
+ return;
289
+ }
290
+ setViewState('ERROR');
291
+ setLastErrorEvent(event.nativeEvent);
292
+ },
293
+ [onError, onLoadEnd]
294
+ );
295
+
296
+ const onLoadingSubResourceError = useCallback(
297
+ (event: WebViewErrorEvent) => {
298
+ onLoadSubResourceError?.(event);
299
+ },
300
+ [onLoadSubResourceError]
301
+ );
302
+
303
+ const onHttpError = useCallback(
304
+ (event: WebViewHttpErrorEvent) => {
305
+ onHttpErrorProp?.(event);
306
+ },
307
+ [onHttpErrorProp]
308
+ );
309
+
310
+ // Android Only
311
+ const onRenderProcessGone = useCallback(
312
+ (event: WebViewRenderProcessGoneEvent) => {
313
+ onRenderProcessGoneProp?.(event);
314
+ },
315
+ [onRenderProcessGoneProp]
316
+ );
317
+ // !Android Only
318
+
319
+ // iOS Only
320
+ const onContentProcessDidTerminate = useCallback(
321
+ (event: WebViewTerminatedEvent) => {
322
+ onContentProcessDidTerminateProp?.(event);
323
+ },
324
+ [onContentProcessDidTerminateProp]
325
+ );
326
+ // !iOS Only
327
+
328
+ const onLoadingFinish = useCallback(
329
+ (event: WebViewNavigationEvent) => {
330
+ onLoad?.(event);
331
+ onLoadEnd?.(event);
332
+ const {
333
+ nativeEvent: { url },
334
+ } = event;
335
+ // on Android, only if url === startUrl
336
+ if (Platform.OS !== 'android' || url === startUrl.current) {
337
+ setViewState('IDLE');
338
+ }
339
+ // !on Android, only if url === startUrl
340
+ updateNavigationState(event);
341
+ },
342
+ [onLoad, onLoadEnd, updateNavigationState]
343
+ );
344
+
345
+ const onMessage = useCallback(
346
+ (event: WebViewMessageEvent) => {
347
+ const { nativeEvent } = event;
348
+ // Exodus: Validate URL against whitelist before processing message
349
+ if (!passesWhitelistCallback(nativeEvent.url)) return;
350
+
351
+ try {
352
+ const parsedData = JSON.parse(nativeEvent.data);
353
+ const data = JSON.stringify(validateData(parsedData));
354
+ const meta = validateMeta(extractMeta(nativeEvent));
355
+
356
+ onMessageProp?.({ ...meta, data });
357
+ } catch (err) {
358
+ console.error('Error parsing WebView message', err);
359
+ }
360
+ },
361
+ [onMessageProp, passesWhitelistCallback, validateData, validateMeta]
362
+ );
363
+
364
+ const onLoadingProgress = useCallback(
365
+ (event: WebViewProgressEvent) => {
366
+ const {
367
+ nativeEvent: { progress },
368
+ } = event;
369
+ // patch for Android only
370
+ if (Platform.OS === 'android' && progress === 1) {
371
+ setViewState((prevViewState) =>
372
+ prevViewState === 'LOADING' ? 'IDLE' : prevViewState
373
+ );
374
+ }
375
+ // !patch for Android only
376
+ onLoadProgress?.(event);
377
+ },
378
+ [onLoadProgress]
379
+ );
380
+
381
+ const onShouldStartLoadWithRequest = useMemo(
382
+ () =>
383
+ createOnShouldStartLoadWithRequest(
384
+ onShouldStartLoadWithRequestCallback,
385
+ originWhitelist,
386
+ deeplinkWhitelist,
387
+ onShouldStartLoadWithRequestProp
388
+ ),
389
+ [
390
+ originWhitelist,
391
+ deeplinkWhitelist,
392
+ onShouldStartLoadWithRequestProp,
393
+ onShouldStartLoadWithRequestCallback,
394
+ ]
395
+ );
396
+
397
+ const onOpenWindow = useCallback(
398
+ (event: WebViewOpenWindowEvent) => {
399
+ onOpenWindowProp?.(event);
400
+ },
401
+ [onOpenWindowProp]
402
+ );
403
+
404
+ return {
405
+ onShouldStartLoadWithRequest,
406
+ onLoadingStart,
407
+ onLoadingProgress,
408
+ onLoadingError,
409
+ onLoadingSubResourceError,
410
+ onLoadingFinish,
411
+ onHttpError,
412
+ onRenderProcessGone,
413
+ onContentProcessDidTerminate,
414
+ onMessage,
415
+ onOpenWindow,
416
+ viewState,
417
+ setViewState,
418
+ lastErrorEvent,
419
+ };
420
+ };
421
+
422
+ /**
423
+ * Exodus: Check if a version string passes the minimum version requirement.
424
+ * Supports complex version constraints like "12.5.6 <13, 13.6.1 <14, 14.8.1 <15, 15.7.1"
425
+ * which means:
426
+ * - 12.5.6 or higher but less than 13
427
+ * - OR 13.6.1 or higher but less than 14
428
+ * - OR 14.8.1 or higher but less than 15
429
+ * - OR 15.7.1 or higher (no upper bound)
430
+ */
431
+ export const versionPasses = (
432
+ version: string | undefined,
433
+ minimum: string | undefined
434
+ ): boolean => {
435
+ if (!version || !minimum) return false;
436
+ if (typeof version !== 'string' || typeof minimum !== 'string') return false;
437
+
438
+ // Handle multiple version ranges separated by ", "
439
+ if (minimum.includes(', ')) {
440
+ const variants = minimum.split(', ');
441
+ // Every entry but the last one should have an upper bound
442
+ if (!variants.slice(0, -1).every((x) => x.includes(' <'))) return false;
443
+ // Any match passes
444
+ return variants.some((x) => versionPasses(version, x));
445
+ }
446
+
447
+ // Handle version range with upper bound (e.g., "12.5.6 <13")
448
+ if (minimum.includes(' <')) {
449
+ const [min, max, ...rest] = minimum.split(' <');
450
+ if (rest.length > 0) return false;
451
+ // Must be >= min AND < max
452
+ // Last check validates that max > min (formatting validation)
453
+ return (
454
+ versionPasses(version, min) &&
455
+ !versionPasses(version, max) &&
456
+ versionPasses(max, version)
457
+ );
458
+ }
459
+
460
+ // Simple version comparison (e.g., "15.7.1")
461
+ const versionRegex = /^[0-9]+(\.[0-9]+)*$/;
462
+ if (!versionRegex.test(version) || !versionRegex.test(minimum)) return false;
463
+
464
+ const versionParts = version.split('.').map(Number);
465
+ const minimumParts = minimum.split('.').map(Number);
466
+ const len = Math.max(versionParts.length, minimumParts.length);
467
+
468
+ for (let i = 0; i < len; i += 1) {
469
+ const ver = versionParts[i] || 0;
470
+ const min = minimumParts[i] || 0;
471
+ if (ver > min) return true;
472
+ if (ver < min) return false;
473
+ }
474
+
475
+ return true; // equals
476
+ };