@exodus/react-native-webview 11.26.1-exodus.9 → 13.16.0-exodus.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -63
- package/android/build.gradle +83 -110
- package/android/gradle.properties +3 -4
- package/android/src/main/AndroidManifest.xml +12 -0
- package/android/src/main/AndroidManifestNew.xml +26 -0
- package/android/src/main/java/com/reactnativecommunity/webview/RNCBasicAuthCredential.java +11 -0
- package/android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java +407 -0
- package/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java +468 -0
- package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewClient.java +330 -0
- package/android/src/main/java/com/reactnativecommunity/webview/{WebViewConfig.java → RNCWebViewConfig.java} +3 -4
- package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewFileProvider.java +1 -1
- package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt +746 -0
- package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewMessagingModule.kt +9 -0
- package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModuleImpl.java +554 -0
- package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewPackage.java +57 -12
- package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewWrapper.kt +39 -0
- package/android/src/main/java/com/reactnativecommunity/webview/events/SubResourceErrorEvent.kt +25 -0
- package/android/src/main/java/com/reactnativecommunity/webview/events/TopCustomMenuSelectionEvent.kt +24 -0
- package/android/src/main/java/com/reactnativecommunity/webview/events/TopHttpErrorEvent.kt +25 -0
- package/android/src/main/java/com/reactnativecommunity/webview/events/TopNewWindowEvent.kt +25 -0
- package/android/src/main/java/com/reactnativecommunity/webview/events/TopRenderProcessGoneEvent.kt +25 -0
- package/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java +570 -0
- package/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewModule.java +57 -0
- package/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java +341 -0
- package/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewModule.java +59 -0
- package/apple/RCTConvert+WKDataDetectorTypes.h +11 -0
- package/apple/RCTConvert+WKDataDetectorTypes.m +27 -0
- package/apple/RNCWebView.h +26 -100
- package/apple/RNCWebView.mm +555 -0
- package/apple/RNCWebViewDecisionManager.h +20 -0
- package/apple/RNCWebViewDecisionManager.m +47 -0
- package/apple/RNCWebViewImpl.h +164 -0
- package/apple/{RNCWebView.m → RNCWebViewImpl.m} +802 -225
- package/apple/RNCWebViewManager.h +4 -8
- package/apple/RNCWebViewManager.mm +221 -0
- package/apple/RNCWebViewModule.h +23 -0
- package/apple/RNCWebViewModule.mm +34 -0
- package/index.d.ts +2 -3
- package/lib/NativeRNCWebViewModule.d.ts +8 -0
- package/lib/NativeRNCWebViewModule.js +1 -0
- package/lib/RNCWebViewNativeComponent.d.ts +245 -0
- package/lib/RNCWebViewNativeComponent.js +1 -0
- package/lib/WebView.android.d.ts +0 -1
- package/lib/WebView.android.js +1 -135
- package/lib/WebView.d.ts +2 -3
- package/lib/WebView.ios.d.ts +0 -1
- package/lib/WebView.ios.js +1 -114
- package/lib/WebView.js +1 -11
- package/lib/WebView.macos.d.ts +6 -0
- package/lib/WebView.macos.js +1 -0
- package/lib/WebView.styles.d.ts +37 -11
- package/lib/WebView.styles.js +1 -33
- package/lib/WebView.windows.d.ts +17 -0
- package/lib/WebView.windows.js +1 -0
- package/lib/WebViewNativeComponent.macos.d.ts +3 -0
- package/lib/WebViewNativeComponent.macos.js +1 -0
- package/lib/WebViewNativeComponent.windows.d.ts +3 -0
- package/lib/WebViewNativeComponent.windows.js +1 -0
- package/lib/WebViewShared.d.ts +30 -9
- package/lib/WebViewShared.js +1 -174
- package/lib/WebViewTypes.d.ts +514 -98
- package/lib/WebViewTypes.js +1 -6
- package/lib/index.d.ts +0 -1
- package/lib/index.js +1 -3
- package/lib/validation.d.ts +3 -0
- package/lib/validation.js +1 -0
- package/package.json +57 -33
- package/react-native-webview.podspec +32 -5
- package/react-native.config.js +22 -18
- package/src/NativeRNCWebViewModule.ts +13 -0
- package/src/RNCWebViewNativeComponent.ts +348 -0
- package/src/WebView.android.tsx +345 -0
- package/src/WebView.ios.tsx +341 -0
- package/src/WebView.macos.tsx +252 -0
- package/src/WebView.styles.ts +41 -0
- package/src/WebView.tsx +25 -0
- package/src/WebView.windows.tsx +217 -0
- package/src/WebViewNativeComponent.macos.ts +7 -0
- package/src/WebViewNativeComponent.windows.ts +8 -0
- package/src/WebViewShared.tsx +476 -0
- package/src/WebViewTypes.ts +1402 -0
- package/src/__tests__/WebViewShared-test.js +323 -0
- package/src/__tests__/__snapshots__/WebViewShared-test.js.snap +8 -0
- package/src/__tests__/validation-test.js +38 -0
- package/src/index.ts +4 -0
- package/src/validation.ts +20 -0
- package/android/.editorconfig +0 -6
- package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java +0 -1408
- package/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java +0 -506
- package/apple/RNCWebViewManager.m +0 -278
- package/lib/WebViewNativeComponent.android.d.ts +0 -4
- package/lib/WebViewNativeComponent.android.js +0 -3
- package/lib/WebViewNativeComponent.ios.d.ts +0 -4
- 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
|
+
};
|