@react-native-ohos/react-native-webview 13.15.3 → 13.15.4-beta.3

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 (59) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/LICENSE +20 -20
  3. package/README.OpenSource +10 -10
  4. package/README.md +395 -13
  5. package/harmony/rn_webview/.gitignore +6 -0
  6. package/harmony/rn_webview/BuildProfile.ets +17 -0
  7. package/harmony/rn_webview/OAT.xml +44 -44
  8. package/harmony/rn_webview/build-profile.json5 +28 -28
  9. package/harmony/rn_webview/index.ets +12 -12
  10. package/harmony/rn_webview/oh-package-lock.json5 +18 -0
  11. package/harmony/rn_webview/oh-package.json5 +13 -13
  12. package/harmony/rn_webview/src/main/cpp/CMakeLists.txt +9 -9
  13. package/harmony/rn_webview/src/main/cpp/WebViewPackage.h +16 -16
  14. package/harmony/rn_webview/src/main/cpp/generated/RNOH/generated/BaseReactNativeWebviewPackage.h +95 -95
  15. package/harmony/rn_webview/src/main/cpp/generated/RNOH/generated/components/RNCWebViewJSIBinder.h +127 -127
  16. package/harmony/rn_webview/src/main/cpp/generated/RNOH/generated/turbo_modules/RNCWebView.cpp +18 -18
  17. package/harmony/rn_webview/src/main/cpp/generated/RNOH/generated/turbo_modules/RNCWebView.h +16 -16
  18. package/harmony/rn_webview/src/main/cpp/generated/RNOH/generated/turbo_modules/RNCWebViewModule.cpp +17 -17
  19. package/harmony/rn_webview/src/main/cpp/generated/RNOH/generated/turbo_modules/RNCWebViewModule.h +16 -16
  20. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/ComponentDescriptors.h +24 -24
  21. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/EventEmitters.cpp +241 -241
  22. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/EventEmitters.h +263 -263
  23. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/Props.cpp +109 -109
  24. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/Props.h +512 -512
  25. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/ShadowNodes.cpp +17 -17
  26. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/ShadowNodes.h +32 -32
  27. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/States.cpp +16 -16
  28. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/States.h +28 -28
  29. package/harmony/rn_webview/src/main/ets/CutomReference.ts +29 -29
  30. package/harmony/rn_webview/src/main/ets/Logger.ts +43 -43
  31. package/harmony/rn_webview/src/main/ets/Magic.ets +200 -200
  32. package/harmony/rn_webview/src/main/ets/RNCWebView.ets +842 -889
  33. package/harmony/rn_webview/src/main/ets/RNCWebViewPackage.ets +37 -37
  34. package/harmony/rn_webview/src/main/ets/ShouldRequestUrl.ts +47 -47
  35. package/harmony/rn_webview/src/main/ets/WebViewBaseOperate.ets +470 -470
  36. package/harmony/rn_webview/src/main/ets/WebViewTurboModule.ets +62 -62
  37. package/harmony/rn_webview/src/main/ets/generated/components/RNCWebView.ts +530 -530
  38. package/harmony/rn_webview/src/main/ets/generated/components/ts.ts +5 -5
  39. package/harmony/rn_webview/src/main/ets/generated/index.ets +5 -5
  40. package/harmony/rn_webview/src/main/ets/generated/ts.ts +6 -6
  41. package/harmony/rn_webview/src/main/ets/generated/turboModules/RNCWebView.ts +18 -18
  42. package/harmony/rn_webview/src/main/ets/generated/turboModules/RNCWebViewModule.ts +16 -16
  43. package/harmony/rn_webview/src/main/ets/generated/turboModules/ts.ts +6 -6
  44. package/harmony/rn_webview/src/main/module.json5 +10 -10
  45. package/harmony/rn_webview/src/main/resources/base/element/string.json +39 -39
  46. package/harmony/rn_webview/src/main/resources/en_US/element/string.json +39 -39
  47. package/harmony/rn_webview/src/main/resources/zh_CN/element/string.json +39 -39
  48. package/harmony/rn_webview/src/test/List.test.ets +4 -4
  49. package/harmony/rn_webview/src/test/LocalUnit.test.ets +32 -32
  50. package/harmony/rn_webview/ts.ets +8 -8
  51. package/harmony/rn_webview.har +0 -0
  52. package/package.json +100 -99
  53. package/src/NativeRNCWebView.ts +21 -21
  54. package/src/NativeRNCWebViewModule.ts +19 -19
  55. package/src/RNCWebViewNativeComponent.ts +357 -357
  56. package/src/WebView.harmony.tsx +344 -344
  57. package/src/WebView.tsx +10 -10
  58. package/src/codegenUtils.ts +10 -10
  59. package/src/index.ts +10 -10
@@ -1,890 +1,843 @@
1
- // Copyright (c) 2025 Huawei Device Co., Ltd. All rights reserved
2
- // Use of this source code is governed by a Apache-2.0 license that can be
3
- // found in the LICENSE file.
4
-
5
- import { RNComponentContext } from '@rnoh/react-native-openharmony';
6
- import webview from '@ohos.web.webview';
7
- import { url as OSUrl } from '@kit.ArkTS';
8
- import { RNC, TM } from './generated';
9
- import Logger from './Logger';
10
- import { BaseOperate } from './WebViewBaseOperate';
11
- import { deviceInfo ,BusinessError} from '@kit.BasicServicesKit';
12
- import {
13
- AlertEvent,
14
- CACHE_MODE,
15
- JAVASCRIPT_INTERFACE,
16
- ONE_HUNDRED,
17
- EIGHT,
18
- ResultType,
19
- RNCWebViewBridge,
20
- WebViewEventParams,
21
- WebViewNewSource,
22
- WebViewOverScrollMode,
23
- ZERO,
24
- COMMAND_NAME,
25
- WebViewDescriptor,
26
- REFRESH_OFFSET,
27
- MINHEIGHT,
28
- WebMediaCapturePermissionGrantType,
29
- MenuItem
30
- } from './Magic';
31
-
32
- import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';
33
- import { WebViewTurboModule, ShouldStartParams, ShouldOverrideCallbackState } from './WebViewTurboModule';
34
- import { AnyThreadRNInstance, WorkerTaskRunnable } from '@rnoh/react-native-openharmony/src/main/ets/RNOH';
35
- import buffer from '@ohos.buffer';
36
-
37
- export const TAG = "WebView"
38
-
39
- export const WEB_VIEW = "RNCWebView"
40
-
41
- @Sendable
42
- class WorkerRunnable implements WorkerTaskRunnable<ShouldStartParams> {
43
- run(rnInstance: AnyThreadRNInstance, params: ShouldStartParams): void {
44
- params.lockState = ShouldOverrideCallbackState.UNDECIDED
45
- rnInstance.getTurboModule<WebViewTurboModule>(TM.RNCWebViewModule.NAME).setShouldStartParams(params)
46
- }
47
- }
48
-
49
- let atManager = abilityAccessCtrl.createAtManager();
50
-
51
- @Component
52
- export struct RNCWebView {
53
- public static readonly NAME = RNC.RNCWebView.NAME
54
- ctx!: RNComponentContext
55
- tag: number = ZERO
56
- source: WebViewNewSource = {
57
- uri: "",
58
- method: "",
59
- body: "",
60
- html: "",
61
- baseUrl: ""
62
- }
63
- html: string | undefined = "";
64
- url: string | Resource = "";
65
- body: string | undefined = "";
66
- controller: webview.WebviewController = new webview.WebviewController();
67
- schemeHandler: webview.WebSchemeHandler = new webview.WebSchemeHandler();
68
- javaScriptEnable: boolean = true;
69
- allowFileAccess: boolean = true;
70
- forceDark: boolean = true;
71
- minFontSize: number = EIGHT;
72
- overScrollMode: OverScrollMode = OverScrollMode.NEVER;
73
- progress: number = ZERO;
74
- cacheMode: number = CacheMode.Default;
75
- requestUrl: string = "";
76
- bundleName: string = "";
77
- messagingEnabled: boolean = false;
78
- hasRegisterJavaScriptProxy: boolean = false;
79
- controllerAttached: boolean = false;
80
- renderMode: RenderMode = RenderMode.SYNC_RENDER;
81
- scrollEnabled = true;
82
- nestedScroll = NestedScrollMode.SELF_FIRST;
83
- headers: Array<webview.WebHeader> = []
84
- injectedJavaScriptBeforeContentLoaded: Array<ScriptItem> = [
85
- { script: '', scriptRules: ["*"] }
86
- ];
87
- allowPageStartInProgress = true;
88
- @State webviewWidth: number = ZERO
89
- @State webviewHeight: number = ZERO
90
- @State isRefreshing: boolean = false
91
- @State mode: MixedMode = MixedMode.All;
92
- private unregisterDescriptorChangesListener?: () => void = undefined
93
- private cleanupCommandCallback?: () => void = undefined
94
- private eventEmitter: RNC.RNCWebView.EventEmitter | undefined = undefined
95
- private cleanUpCallbacks: (() => void)[] = []
96
- private descriptorWrapper: WebViewDescriptor = Object() as WebViewDescriptor
97
- private webViewBaseOperate: BaseOperate | null = null
98
- private shouldInterceptLoad: boolean = true
99
- private callLoadTimer: number = -1
100
- static readonly SHOULD_OVERRIDE_URL_LOADING_TIMEOUT: number = 250
101
- private debounceTimer: number = -1
102
- static readonly DEBOUNCE_TIME: number = 100
103
- private isFirstDebounce: boolean = true
104
- private isMainFrameLoaded:boolean = false;
105
- private isCatchMainFrameError:boolean = false;
106
- private isAlreadyRunInject:boolean = false;
107
-
108
- aboutToAppear() {
109
- try {
110
- this.initVariable()
111
- this.getBundleName()
112
- this.url = this.source.uri as string;
113
- this.onDescriptorWrapperChange(this.descriptorWrapper)
114
- this.unregisterDescriptorChangesListener = this.ctx.descriptorRegistry.subscribeToDescriptorChanges(this.tag,
115
- (newDescriptor) => {
116
- this.onDescriptorWrapperChange(newDescriptor as WebViewDescriptor)
117
- }
118
- )
119
- BaseOperate.setThirdPartyCookiesEnabled(this.descriptorWrapper.rawProps.thirdPartyCookiesEnabled)
120
- webview.WebviewController.setWebDebuggingAccess(this.descriptorWrapper.rawProps.webviewDebuggingEnabled)
121
- this.registerCommandCallback()
122
- if (this.hasIncognito()) {
123
- this.cacheMode = CacheMode.Online;
124
- }
125
- if (this.source.headers) {
126
- this.source.headers.forEach(item => {
127
- if (item.name && item.value) {
128
- this.headers.push({ headerKey: item.name, headerValue: item.value })
129
- }
130
- })
131
- }
132
- if (this.descriptorWrapper.rawProps.geolocationEnabled) {
133
- atManager.requestPermissionsFromUser(this.ctx.uiAbilityContext, ["ohos.permission.APPROXIMATELY_LOCATION"]).then((data) => {
134
- }).catch((error: BusinessError) => {
135
- console.error(`Failed to request permissions from user. Code is ${error.code}, message is ${error.message}`);
136
- })
137
- }
138
- } catch (error) {
139
- Logger.error(TAG, `[RNOH]Errorcode: ${error.code}, Message: ${error.message}`);
140
- }
141
-
142
- }
143
-
144
- hasIncognito(): boolean {
145
- return this.descriptorWrapper.rawProps.incognito && this.descriptorWrapper.rawProps.incognito === true
146
- }
147
-
148
- createWebViewEvent(type: string): WebViewEventParams {
149
- return this.webViewBaseOperate?.createWebViewEvent({ type, progress: this.progress }) as WebViewEventParams;
150
- }
151
-
152
- aboutToDisappear() {
153
- this.getUIContext().getFocusController().clearFocus()
154
- this.cleanupCommandCallback?.()
155
- this.unregisterDescriptorChangesListener?.()
156
- this.cleanUpCallbacks.forEach(cb => cb())
157
- try {
158
- if (this.hasIncognito()) {
159
- this.webViewBaseOperate?.setIncognito()
160
- }
161
- this.controller.deleteJavaScriptRegister(JAVASCRIPT_INTERFACE)
162
- this.controller.refresh()
163
- this.hasRegisterJavaScriptProxy = false
164
- this.webViewBaseOperate = null
165
- } catch (error) {
166
- Logger.error(TAG, `[RNOH]Errorcode: ${error.code}, Message: ${error.message}`);
167
- }
168
- }
169
-
170
- registerCommandCallback() {
171
- this.cleanupCommandCallback = this.ctx.componentCommandReceiver.registerCommandCallback(
172
- this.tag,
173
- (command: COMMAND_NAME, args: string[]) => this.webViewBaseOperate?.registerCommandCallback(command, args));
174
- }
175
-
176
- onLoadingStart() {
177
- this.isMainFrameLoaded = false;
178
- this.isCatchMainFrameError = false;
179
- this.webViewBaseOperate?.emitLoadingStart({ progress: this.progress })
180
- }
181
-
182
- onProgressChange(event: OnProgressChangeEvent) {
183
- this.progress = event.newProgress
184
- // 修复单页面应用切换路由时 onPageBegin 未触发的问题
185
- if (this.controller.getUrl() !== this.url && this.progress < ONE_HUNDRED && this.allowPageStartInProgress) {
186
- this.allowPageStartInProgress = false;
187
- this.onLoadingStart();
188
- }
189
- this.webViewBaseOperate?.emitProgressChange({ progress: this.progress })
190
- if (this.progress === ONE_HUNDRED) {
191
- this.allowPageStartInProgress = true;
192
-
193
- if (deviceInfo.sdkApiVersion >= 20 && this.isMainFrameLoaded && !this.isCatchMainFrameError) {
194
- if (this.isAlreadyRunInject) {
195
- this.onLoadFinished()
196
- this.isAlreadyRunInject = false;
197
- }
198
- }
199
- }
200
- }
201
-
202
- onLoadingFinish() {
203
- this.webViewBaseOperate?.emitLoadingFinish({ progress: this.progress })
204
- this.isRefreshing = false
205
- }
206
-
207
- runInjectedJavaScript() {
208
- this.isAlreadyRunInject = true;
209
- let injectedJS = this.descriptorWrapper.rawProps.injectedJavaScript
210
- if (this.javaScriptEnable && injectedJS != "" && this.controllerAttached) {
211
- try {
212
- this.controller.runJavaScript("(function() {\n" + injectedJS + ";\n})();")
213
- .then((result) => {
214
- Logger.debug(TAG, '[RNOH] result: ' + result);
215
- })
216
- .catch((error: string | Error) => {
217
- Logger.error(TAG, "[RNOH] error: " + error);
218
- })
219
- } catch (error) {
220
- Logger.error(TAG, `[RNOH]Errorcode: ${error.code}, Message: ${error.message}`);
221
- }
222
- }
223
- }
224
-
225
- getJsDialogTitle() {
226
- const url = this.controller.getUrl()
227
- // 对于 data: URL,只显示 'JavaScript',类似于 Chrome
228
- let title = 'JavaScript';
229
- const isDataUrl = (url !== null) && url.startsWith("data:");
230
-
231
-
232
- if (!isDataUrl) {
233
- try {
234
- const dialogUrl = OSUrl.URL.parseURL(url);
235
- title = `网址为”${dialogUrl.protocol}//${dialogUrl.host}“的网页显示:`;
236
- } catch (ex) {
237
- title = url;
238
- }
239
- }
240
-
241
- return title;
242
- }
243
-
244
- onJavascriptAlert(event?: AlertEvent) {
245
- if (event) {
246
- AlertDialog.show({
247
- title: this.getJsDialogTitle(),
248
- message: event.message,
249
- alignment: DialogAlignment.Center,
250
- secondaryButton: {
251
- value: this.resourceToString($r('app.string.determined')),
252
- action: () => event.result.handleConfirm()
253
- },
254
- cancel: () => event.result.handleCancel(),
255
- })
256
- }
257
- return true
258
- }
259
-
260
- onJavascriptConfirm(event?: AlertEvent) {
261
- if (event) {
262
- AlertDialog.show({
263
- title: this.getJsDialogTitle(),
264
- message: event.message,
265
- alignment: DialogAlignment.Center,
266
- secondaryButton: {
267
- value: this.resourceToString($r('app.string.determined')),
268
- action: () => event.result.handleConfirm()
269
- },
270
- cancel: () => event.result.handleCancel(),
271
- primaryButton: {
272
- value: this.resourceToString($r('app.string.cancel')),
273
- action: () => event.result.handleCancel()
274
- },
275
- })
276
- }
277
- return true
278
- }
279
-
280
- ignoreSilentHardwareSwitchMethods(ignoreSilentHardwareSwitch: boolean) {
281
- this.webViewBaseOperate?.ignoreSilentHardwareSwitchMethods(ignoreSilentHardwareSwitch)
282
- }
283
-
284
- loadHtmlData(html: string = '', url: string = '') {
285
- try {
286
- this.controller.loadData(
287
- html,
288
- "text/html",
289
- "UTF-8",
290
- url,
291
- " "
292
- );
293
- } catch (error) {
294
- Logger.error(TAG, "error:" + error)
295
- }
296
- }
297
-
298
- controllerAttachedInit(): void {
299
- this.controllerAttached = true;
300
- this.eventEmitter = new RNC.RNCWebView.EventEmitter(this.ctx.rnInstance, this.tag)
301
- this.webViewBaseOperate = new BaseOperate(this.eventEmitter, this.controller)
302
- this.webViewBaseOperate.setCustomUserAgent(this.descriptorWrapper.rawProps.userAgent,
303
- this.descriptorWrapper.rawProps.applicationNameForUserAgent)
304
- this.webViewBaseOperate.setFraudulentWebsiteWarningEnabled(this.descriptorWrapper.rawProps.fraudulentWebsiteWarningEnabled)
305
-
306
- let schemeList = this.descriptorWrapper.rawProps.schemeList
307
- if(schemeList?.length){
308
- schemeList.forEach(scheme=>{
309
- this.schemeHandler.onRequestStart((request: webview.WebSchemeHandlerRequest, resourceHandler: webview.WebResourceHandler) => {
310
- console.log("[schemeHandler] onRequestStart");
311
- return true;
312
- })
313
- this.schemeHandler.onRequestStop((request: webview.WebSchemeHandlerRequest) => {
314
- console.log("[schemeHandler] onRequestStop");
315
- });
316
- this.controller.setWebSchemeHandler(scheme, this.schemeHandler);
317
- })
318
- }
319
-
320
- let baseUrl = this.source.baseUrl
321
- let uri = this.source.uri
322
- if (this.source.html != undefined && this.source.html != "") {
323
- this.loadHtmlData(this.source.html, baseUrl)
324
- } else if (uri != undefined && uri != "") {
325
- if (this.source.method && this.source.method.toUpperCase() === 'POST' && this.source.body) {
326
- let postData = buffer.from(this.source.body);
327
- let url = uri as string;
328
- this.controller.postUrl(url, postData?.buffer);
329
- } else {
330
- this.controller.loadUrl(uri, this.headers);
331
- }
332
- } else {
333
- this.loadHtmlData()
334
- }
335
- if (!this.hasRegisterJavaScriptProxy) {
336
- this.registerPostMessage()
337
- }
338
- }
339
-
340
- onPageBegin() {
341
- try {
342
- if (this.controller.getUrl() === this.url) {
343
- this.onLoadingStart();
344
- }
345
- this.controller.setScrollable(this.scrollEnabled)
346
- this.ignoreSilentHardwareSwitchMethods(this.descriptorWrapper.rawProps.ignoreSilentHardwareSwitch)
347
- } catch (error) {
348
- Logger.debug(TAG,
349
- `[RNOH] setignoreSilentHardwareSwitch and setScrollable ErrorCode: ${error.code}, Message: ${error.message}, userAgent: ${this.descriptorWrapper.rawProps.userAgent}`);
350
- }
351
- }
352
-
353
- getBundleName() {
354
- let bundleFlags =
355
- bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_HAP_MODULE | bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_ABILITY |
356
- bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION;
357
-
358
- try {
359
- bundleManager.getBundleInfoForSelf(bundleFlags, (error, data) => {
360
- if (error) {
361
- Logger.error(TAG,
362
- `[RNOH] getBundleInfoForSelf failed ErrorCode: ${error.code}, Message: ${error.message}`);
363
- } else {
364
- this.bundleName = JSON.stringify(data.name)
365
- }
366
- });
367
- } catch (error) {
368
- Logger.error(TAG,
369
- `[RNOH] getBundleInfoForSelf failed ErrorCode: ${error.code}, Message: ${error.message}`);
370
- }
371
- }
372
-
373
- resourceToString(resource: Resource): string {
374
- return getContext(this).resourceManager.getStringSync(resource)
375
- }
376
-
377
- onLoadFinished() {
378
- this.onLoadingFinish()
379
- }
380
-
381
- onPageEndExecuteRunInjectedJavaScript(){
382
- this.runInjectedJavaScript()
383
- }
384
-
385
- onPageEnd() {
386
- this.runInjectedJavaScript()
387
- this.onLoadingFinish()
388
- }
389
-
390
- getPermissionDialogMessage(event: OnPermissionRequestEvent) {
391
- let permissionDialogMessage = ''
392
- permissionDialogMessage = event.request.getAccessibleResource().toString()
393
- switch (permissionDialogMessage) {
394
- case ProtectedResourceType.VIDEO_CAPTURE:
395
- permissionDialogMessage = this.resourceToString($r('app.string.use_your_camera'))
396
- break;
397
- case ProtectedResourceType.AUDIO_CAPTURE:
398
- permissionDialogMessage = this.resourceToString($r('app.string.use_your_microphone'))
399
- break;
400
- case ProtectedResourceType.MidiSysex:
401
- permissionDialogMessage = this.resourceToString($r('app.string.midi_sysex_resources'))
402
- break;
403
- case ProtectedResourceType.VIDEO_CAPTURE + "," + ProtectedResourceType.AUDIO_CAPTURE:
404
- permissionDialogMessage = this.resourceToString($r('app.string.use_your_camera_and_microphone'))
405
- break;
406
- }
407
- return permissionDialogMessage;
408
- }
409
- originWhitelistToRegex(originWhitelist: string): string {
410
- const matchOperatorsRegex = /[|\\{}()[\]^$+*?.-]/g;
411
- return `^${originWhitelist.replace(matchOperatorsRegex, '\\$&').replace(/\\\*/g, '.*')}`;
412
- }
413
- extractOrigin(url: string): string {
414
- const result = /^[A-Za-z][A-Za-z0-9+\-.]+:(\/\/)?[^/]*/.exec(url);
415
- return result === null ? '' : result[0];
416
- };
417
- passesWhitelist(compiledWhitelist: string[], url: string) {
418
- const origin = this.extractOrigin(url);
419
- return compiledWhitelist.some((x) => new RegExp(x).test(origin));
420
- };
421
- compileWhitelist(originWhitelist: string[]): string[] {
422
- return ['about:blank', ...(originWhitelist || [])].map(this.originWhitelistToRegex);
423
- }
424
-
425
- onLoadIntercept(event: OnLoadInterceptEvent): boolean {
426
- Logger.debug(TAG, `onLoadIntercept start`)
427
- let originWhitelistArr = this.descriptorWrapper.rawProps.originWhitelist;
428
- let hasOnShouldStartLoadWithRequestEventBool = this.descriptorWrapper.rawProps.hasOnShouldStartLoadWithRequestEvent
429
- let urlString =
430
- this.webViewBaseOperate?.verifyURLFormat(event.data.getRequestUrl(), this.source.html, this.source.baseUrl) || "";
431
-
432
- if (this.passesWhitelist(this.compileWhitelist(originWhitelistArr), urlString) &&
433
- !hasOnShouldStartLoadWithRequestEventBool) {
434
- return false
435
- }
436
- this.webViewBaseOperate?.setLockIdentifier(BaseOperate.generateLockIdentifier())
437
-
438
- const params = new ShouldStartParams();
439
- params.lockIdentifier = this.webViewBaseOperate?.getLockIdentifier() || -1
440
- this.ctx.runOnWorkerThread(new WorkerRunnable(), params);
441
- this.webViewBaseOperate?.emitShouldStartLoadWithRequest(event, this.source.html, this.source.baseUrl)
442
- const startTime = Date.now();
443
- // 当lockState没变化并且小于超时时间,阻塞进程
444
- while (params.lockState === ShouldOverrideCallbackState.UNDECIDED &&
445
- Date.now() - startTime < RNCWebView.SHOULD_OVERRIDE_URL_LOADING_TIMEOUT) {
446
- // 空循环等待
447
- }
448
-
449
- Logger.debug('params.lockState res:' + params.lockState + `, wait time is ${Date.now() - startTime}`);
450
- return params.lockState === ShouldOverrideCallbackState.SHOULD_OVERRIDE;
451
- }
452
-
453
- onOverrideUrlLoading(event: WebResourceRequest) {
454
- this.webViewBaseOperate?.emitShouldStartLoadWithRequestOverrideUrlLoading(event)
455
- return false
456
- }
457
-
458
- onCreateMenu = (menuItems: Array<TextMenuItem>) => {
459
- let customMenuItems = this.descriptorWrapper.rawProps.menuItems
460
- if (customMenuItems == undefined) {
461
- return menuItems;
462
- } else if (customMenuItems.length == 0) {
463
- return [];
464
- } else {
465
- let menuItemsArray: Array<TextMenuItem> = [];
466
- for (let index = 0; index < customMenuItems.length; index++) {
467
- let element: MenuItem = customMenuItems[index];
468
- let customItem: TextMenuItem = {
469
- content: element.label,
470
- id: TextMenuItemId.of(element.key)
471
- };
472
- menuItemsArray.push(customItem)
473
- }
474
- return menuItemsArray;
475
- }
476
- }
477
-
478
- onMenuItemClick(menuItem: TextMenuItem): boolean {
479
- if (this.descriptorWrapper.rawProps.menuItems != undefined) {
480
- this.controller.runJavaScript("(function(){return {selection: window.getSelection().toString()} })()")
481
- .then((result: string) => {
482
- this.webViewBaseOperate?.customMenuSelection(menuItem, result)
483
- })
484
- .catch((error: string | Error) => {
485
- Logger.info('[RNOH] function onMenuItemClick error: ' + error);
486
- })
487
- // 禁用系统默认行为
488
- return true;
489
- }
490
- return false;
491
- }
492
-
493
- onPermissionRequestAlert(event: OnPermissionRequestEvent){
494
- AlertDialog.show({
495
- title: this.resourceToString($r('app.string.on_confirm')) + event.request.getOrigin(),
496
- message: this.getPermissionDialogMessage(event),
497
- alignment: DialogAlignment.Center,
498
- primaryButton: {
499
- value: $r('app.string.deny'),
500
- action: () => {
501
- event.request.deny();
502
- }
503
- },
504
- secondaryButton: {
505
- value: $r('app.string.on_confirm'),
506
- action: () => {
507
- event.request.grant(event.request.getAccessibleResource());
508
- }
509
- },
510
- cancel: () => {
511
- event.request.deny();
512
- }
513
- })
514
- }
515
-
516
- async handleMediaCapturePermissionGrantType(resources: string[], domain: string) {
517
- let permissionList: Array<Permissions> = [];
518
- resources.forEach(resource => {
519
- switch (resource.toString()) {
520
- case 'TYPE_VIDEO_CAPTURE':
521
- permissionList.push('ohos.permission.CAMERA');
522
- break;
523
- case 'TYPE_AUDIO_CAPTURE':
524
- permissionList.push('ohos.permission.MICROPHONE');
525
- break;
526
- }
527
- });
528
-
529
- await atManager.requestPermissionsFromUser(this.ctx.uiAbilityContext, permissionList);
530
- }
531
-
532
- build() {
533
- Stack() {
534
- if ( deviceInfo.sdkApiVersion >= 20) {
535
- Web({ src: "", controller: this.controller, renderMode: this.renderMode })
536
- .mixedMode(this.mode)
537
- .onPermissionRequest(async (event: OnPermissionRequestEvent) => {
538
- if (event && (this.getPermissionDialogMessage(event) != "TYPE_SENSOR")) {
539
- const resources: string[] = event.request.getAccessibleResource();
540
- if (resources.indexOf('TYPE_AUDIO_CAPTURE') >= 0 || resources.indexOf('TYPE_VIDEO_CAPTURE') >= 0 ) {
541
- const originUrl = this.webViewBaseOperate?.verifyURLFormat(event.request.getOrigin(), this.source.html,
542
- this.source.baseUrl);
543
- const currentUrl = this.webViewBaseOperate?.verifyURLFormat(this.controller.getUrl(), this.source.html,
544
- this.source.baseUrl);
545
- const originDomain: string = OSUrl.URL.parseURL(originUrl).hostname;
546
- const currentDomain: string = OSUrl.URL.parseURL(currentUrl).hostname;
547
- let mediaCapturePermissionGrantType: string = WebMediaCapturePermissionGrantType.PROMPT
548
- if (this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType != undefined) {
549
- mediaCapturePermissionGrantType = this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType;
550
- }
551
- if (mediaCapturePermissionGrantType != WebMediaCapturePermissionGrantType.DENY) {
552
- await this.handleMediaCapturePermissionGrantType(resources, originDomain)
553
- }
554
- switch (mediaCapturePermissionGrantType) {
555
- case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_PROMPT:
556
- if (originDomain != currentDomain) {
557
- this.onPermissionRequestAlert(event)
558
- } else {
559
- event.request.grant(resources);
560
- }
561
- break;
562
- case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_DENY:
563
- if (originDomain != currentDomain) {
564
- event.request.deny();
565
- } else {
566
- event.request.grant(resources);
567
- }
568
- break;
569
- case WebMediaCapturePermissionGrantType.DENY:
570
- event.request.deny();
571
- break;
572
- case WebMediaCapturePermissionGrantType.GRANT:
573
- event.request.grant(resources);
574
- break;
575
- case WebMediaCapturePermissionGrantType.PROMPT:
576
- this.onPermissionRequestAlert(event)
577
- break;
578
- }
579
- } else {
580
- this.onPermissionRequestAlert(event)
581
- }
582
- }
583
- })
584
- .width(this.webviewWidth)
585
- .height(this.webviewHeight)
586
- .fileAccess(this.allowFileAccess)
587
- .constraintSize({ minHeight: MINHEIGHT })
588
- .overScrollMode(this.overScrollMode)
589
- .javaScriptAccess(this.javaScriptEnable)
590
- .javaScriptOnDocumentStart(this.injectedJavaScriptBeforeContentLoaded)
591
- .horizontalScrollBarAccess(this.descriptorWrapper.rawProps.showsHorizontalScrollIndicator)
592
- .verticalScrollBarAccess(this.descriptorWrapper.rawProps.showsVerticalScrollIndicator)
593
- .overviewModeAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
594
- .textZoomRatio(this.descriptorWrapper.rawProps.textZoom)
595
- .backgroundColor(BaseOperate.getColorMode(this.ctx.uiAbilityContext,
596
- this.descriptorWrapper.rawProps.forceDarkOn) === WebDarkMode.On ? Color.White : Color.Transparent)
597
- .darkMode(BaseOperate.getColorMode(this.ctx.uiAbilityContext, this.descriptorWrapper.rawProps.forceDarkOn))
598
- .forceDarkAccess(this.forceDark)
599
- .cacheMode(this.cacheMode)
600
- .minFontSize(this.minFontSize)
601
- .domStorageAccess(this.descriptorWrapper.rawProps.domStorageEnabled)
602
- .zoomAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
603
- .overScrollMode(this.overScrollMode)
604
- .onProgressChange((event: OnProgressChangeEvent) => this.onProgressChange(event))
605
- .onScroll((event: OnScrollEvent) => this.webViewBaseOperate?.emitScroll(event))
606
- .nestedScroll({
607
- scrollForward: this.nestedScroll,
608
- scrollBackward: this.nestedScroll,
609
- })
610
- .onLoadStarted(() => {
611
- this.isMainFrameLoaded = false;
612
- this.isCatchMainFrameError = false;
613
- this.onPageBegin()
614
- })
615
- .onPageEnd(() => {
616
- this.isMainFrameLoaded = true;
617
- if (this.isCatchMainFrameError) {
618
- if (this.source.html) {
619
- this.onLoadFinished();
620
- }
621
- return;
622
- }
623
- this.onPageEndExecuteRunInjectedJavaScript()
624
-
625
- })
626
- .onErrorReceive((event: OnErrorReceiveEvent) => {
627
- const isMainFrame = event.request.isMainFrame();
628
- if (isMainFrame) {
629
- this.isCatchMainFrameError = true;
630
- }
631
- this.webViewBaseOperate?.emitLoadingError(event)
632
- })
633
- .onHttpErrorReceive((event: OnHttpErrorReceiveEvent) => {
634
- const isMainFrame = event.request.isMainFrame();
635
- if (isMainFrame) {
636
- this.isCatchMainFrameError = true;
637
- }
638
- this.webViewBaseOperate?.emitHttpError(event)
639
- })
640
- .onControllerAttached(() => this.controllerAttachedInit())
641
- .onAlert((event: AlertEvent) => this.onJavascriptAlert(event))
642
- .onConfirm((event: AlertEvent) => this.onJavascriptConfirm(event))
643
- .onDownloadStart((event: OnDownloadStartEvent) => this.webViewBaseOperate?.onDownloadStart(event))
644
- .geolocationAccess(this.descriptorWrapper.rawProps.geolocationEnabled)
645
- .onGeolocationShow((event) => {
646
- if (event && (this.descriptorWrapper.rawProps.geolocationEnabled != undefined)) {
647
- event.geolocation.invoke(event.origin, this.descriptorWrapper.rawProps.geolocationEnabled, true);
648
- }
649
- })
650
- .mediaPlayGestureAccess(this.descriptorWrapper.rawProps.mediaPlaybackRequiresUserAction)
651
- .onRenderExited((event: OnRenderExitedEvent) => this.webViewBaseOperate?.onRenderExited(event))
652
- .onLoadIntercept((event: OnLoadInterceptEvent) => this.onLoadIntercept(event))
653
- .onTitleReceive((event: OnTitleReceiveEvent) => this.webViewBaseOperate?.onTitleReceive(event))
654
- .editMenuOptions({
655
- onCreateMenu: this.onCreateMenu.bind(this),
656
- onMenuItemClick: this.onMenuItemClick.bind(this),
657
- })
658
- } else {
659
- Web({ src: "", controller: this.controller, renderMode: this.renderMode })
660
- .mixedMode(this.mode)
661
- .onPermissionRequest(async (event: OnPermissionRequestEvent) => {
662
- if (event && (this.getPermissionDialogMessage(event) != "TYPE_SENSOR")) {
663
- const resources: string[] = event.request.getAccessibleResource();
664
- if (resources.indexOf('TYPE_AUDIO_CAPTURE') >= 0 || resources.indexOf('TYPE_VIDEO_CAPTURE') >= 0 ) {
665
- const originUrl = this.webViewBaseOperate?.verifyURLFormat(event.request.getOrigin(), this.source.html,
666
- this.source.baseUrl);
667
- const currentUrl = this.webViewBaseOperate?.verifyURLFormat(this.controller.getUrl(), this.source.html,
668
- this.source.baseUrl);
669
- const originDomain: string = OSUrl.URL.parseURL(originUrl).hostname;
670
- const currentDomain: string = OSUrl.URL.parseURL(currentUrl).hostname;
671
- let mediaCapturePermissionGrantType: string = WebMediaCapturePermissionGrantType.PROMPT
672
- if (this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType != undefined) {
673
- mediaCapturePermissionGrantType = this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType;
674
- }
675
- if (mediaCapturePermissionGrantType != WebMediaCapturePermissionGrantType.DENY) {
676
- await this.handleMediaCapturePermissionGrantType(resources, originDomain)
677
- }
678
- switch (mediaCapturePermissionGrantType) {
679
- case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_PROMPT:
680
- if (originDomain != currentDomain) {
681
- this.onPermissionRequestAlert(event)
682
- } else {
683
- event.request.grant(resources);
684
- }
685
- break;
686
- case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_DENY:
687
- if (originDomain != currentDomain) {
688
- event.request.deny();
689
- } else {
690
- event.request.grant(resources);
691
- }
692
- break;
693
- case WebMediaCapturePermissionGrantType.DENY:
694
- event.request.deny();
695
- break;
696
- case WebMediaCapturePermissionGrantType.GRANT:
697
- event.request.grant(resources);
698
- break;
699
- case WebMediaCapturePermissionGrantType.PROMPT:
700
- this.onPermissionRequestAlert(event)
701
- break;
702
- }
703
- } else {
704
- this.onPermissionRequestAlert(event)
705
- }
706
- }
707
- })
708
-
709
- .width(this.webviewWidth)
710
- .height(this.webviewHeight)
711
- .fileAccess(this.allowFileAccess)
712
- .constraintSize({ minHeight: MINHEIGHT })
713
- .overScrollMode(this.overScrollMode)
714
- .javaScriptAccess(this.javaScriptEnable)
715
- .javaScriptOnDocumentStart(this.injectedJavaScriptBeforeContentLoaded)
716
- .horizontalScrollBarAccess(this.descriptorWrapper.rawProps.showsHorizontalScrollIndicator)
717
- .verticalScrollBarAccess(this.descriptorWrapper.rawProps.showsVerticalScrollIndicator)
718
- .overviewModeAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
719
- .textZoomRatio(this.descriptorWrapper.rawProps.textZoom)
720
- .backgroundColor(BaseOperate.getColorMode(this.ctx.uiAbilityContext,
721
- this.descriptorWrapper.rawProps.forceDarkOn) === WebDarkMode.On ? Color.White : Color.Transparent)
722
- .darkMode(BaseOperate.getColorMode(this.ctx.uiAbilityContext, this.descriptorWrapper.rawProps.forceDarkOn))
723
- .forceDarkAccess(this.forceDark)
724
- .cacheMode(this.cacheMode)
725
- .minFontSize(this.minFontSize)
726
- .domStorageAccess(this.descriptorWrapper.rawProps.domStorageEnabled)
727
- .zoomAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
728
- .overScrollMode(this.overScrollMode)
729
- .onProgressChange((event: OnProgressChangeEvent) => this.onProgressChange(event))
730
- .onScroll((event: OnScrollEvent) => this.webViewBaseOperate?.emitScroll(event))
731
- .nestedScroll({
732
- scrollForward: this.nestedScroll,
733
- scrollBackward: this.nestedScroll,
734
- })
735
- .onPageBegin(() => this.onPageBegin())
736
- .onPageEnd(() => this.onPageEnd())
737
- .onErrorReceive((event: OnErrorReceiveEvent) => this.webViewBaseOperate?.emitLoadingError(event))
738
- .onHttpErrorReceive((event: OnHttpErrorReceiveEvent) => this.webViewBaseOperate?.emitHttpError(event))
739
- .onControllerAttached(() => this.controllerAttachedInit())
740
- .onAlert((event: AlertEvent) => this.onJavascriptAlert(event))
741
- .onConfirm((event: AlertEvent) => this.onJavascriptConfirm(event))
742
- .onDownloadStart((event: OnDownloadStartEvent) => this.webViewBaseOperate?.onDownloadStart(event))
743
- .geolocationAccess(this.descriptorWrapper.rawProps.geolocationEnabled)
744
- .onGeolocationShow((event) => {
745
- if (event && (this.descriptorWrapper.rawProps.geolocationEnabled != undefined)) {
746
- event.geolocation.invoke(event.origin, this.descriptorWrapper.rawProps.geolocationEnabled, true);
747
- }
748
- })
749
- .mediaPlayGestureAccess(this.descriptorWrapper.rawProps.mediaPlaybackRequiresUserAction)
750
- .onRenderExited((event: OnRenderExitedEvent) => this.webViewBaseOperate?.onRenderExited(event))
751
- .onLoadIntercept((event: OnLoadInterceptEvent) => this.onLoadIntercept(event))
752
- .onTitleReceive((event: OnTitleReceiveEvent) => this.webViewBaseOperate?.onTitleReceive(event))
753
- .editMenuOptions({
754
- onCreateMenu: this.onCreateMenu.bind(this),
755
- onMenuItemClick: this.onMenuItemClick.bind(this),
756
- })
757
- }
758
- }
759
- .width(this.webviewWidth)
760
- .height(this.webviewHeight)
761
- .position({
762
- x: this.descriptorWrapper.layoutMetrics.frame.origin.x,
763
- y: this.descriptorWrapper.layoutMetrics.frame.origin.y
764
- })
765
- }
766
-
767
- private onDescriptorWrapperChange(descriptorWrapper: WebViewDescriptor) {
768
- this.initVariable()
769
- this.descriptorWrapper = descriptorWrapper
770
- if (this.source.html != "" && this.html != this.source.html) {
771
- if (this.controllerAttached) {
772
- this.loadHtmlData(this.source.html, this.source.baseUrl)
773
- Logger.debug(TAG, "[RNOH] html is update")
774
- this.html = this.source.html
775
- }
776
- } else if (this.source.uri && this.source.method && this.source.method.toUpperCase() === 'POST' && this.source.body && (this.source.uri != this.url || this.body != this.source.body)) {
777
- if (this.controllerAttached) {
778
- let url = this.source.uri as string;
779
- this.url = url;
780
- this.body = this.source.body;
781
- let postData = buffer.from(this.source.body);
782
- this.controller.postUrl(url, postData?.buffer);
783
- }
784
- } else if (this.url != this.source.uri) {
785
- if (this.source.uri != "") {
786
- Logger.debug(TAG, `[RNOH] newDescriptor props update uri: ` + this.source.uri);
787
- if (this.controllerAttached) {
788
- this.controller.loadUrl(this.descriptorWrapper.rawProps.newSource.uri, this.headers)
789
- }
790
- } else {
791
- this.loadHtmlData()
792
- }
793
- this.url = this.source.uri as string;
794
- }
795
- if (this.controllerAttached) {
796
- this.controller?.setScrollable(this.descriptorWrapper.rawProps.scrollEnabled)
797
- }
798
- }
799
-
800
- private initVariable() {
801
- this.descriptorWrapper = this.ctx.descriptorRegistry.getDescriptor<WebViewDescriptor>(this.tag)
802
- this.javaScriptEnable = this.descriptorWrapper.rawProps.javaScriptEnabled;
803
- this.minFontSize =
804
- this.descriptorWrapper.rawProps.minimumFontSize ? this.descriptorWrapper.rawProps.minimumFontSize : EIGHT;
805
- this.cacheMode =
806
- this.descriptorWrapper.rawProps.cacheEnabled ?
807
- this.webViewBaseOperate?.transCacheMode(this.descriptorWrapper.rawProps.cacheMode as CACHE_MODE) as number :
808
- CacheMode.Online;
809
- this.source = {
810
- uri: this.descriptorWrapper.rawProps.newSource.uri ?? "",
811
- method: this.descriptorWrapper.rawProps.newSource.method ?? "",
812
- body: this.descriptorWrapper.rawProps.newSource.body ?? "",
813
- html: this.descriptorWrapper.rawProps.newSource.html ?? "",
814
- baseUrl: this.descriptorWrapper.rawProps.newSource.baseUrl ?? "",
815
- headers: this.descriptorWrapper.rawProps.newSource.headers ?? []
816
- }
817
- if (this.source.headers) {
818
- this.source.headers.forEach(item => {
819
- if (item.name && item.value) {
820
- this.headers.push({ headerKey: item.name, headerValue: item.value })
821
- }
822
- })
823
- }
824
- // this.html = this.source.html
825
- const newWidth = this.descriptorWrapper.layoutMetrics.frame.size.width
826
- const newHeight = this.descriptorWrapper.layoutMetrics.frame.size.height
827
-
828
- if (this.debounceTimer !== -1) {
829
- clearTimeout(this.debounceTimer)
830
- this.debounceTimer = -1
831
- }
832
-
833
- if (this.isFirstDebounce) {
834
- this.webviewWidth = newWidth
835
- this.webviewHeight = newHeight
836
- this.isFirstDebounce = false
837
- } else {
838
- this.debounceTimer = setTimeout(() => {
839
- this.webviewWidth = newWidth
840
- this.webviewHeight = newHeight
841
- }, RNCWebView.DEBOUNCE_TIME)
842
- }
843
- this.scrollEnabled = this.descriptorWrapper.rawProps.scrollEnabled;
844
- // 当禁止滚动时,使用父组件滚动
845
- this.nestedScroll = this.scrollEnabled ? NestedScrollMode.SELF_FIRST : NestedScrollMode.PARENT_FIRST;
846
- if (this.source.uri && this.source.uri.toString().startsWith("asset://")) {
847
- this.source.uri = $rawfile(this.source.uri.toString().replace("asset://", this.ctx.rnInstance.getAssetsDest()));
848
- }
849
-
850
- if (this.descriptorWrapper.rawProps.overScrollMode === WebViewOverScrollMode.ALWAYS) {
851
- this.overScrollMode = OverScrollMode.ALWAYS
852
- } else if (this.descriptorWrapper.rawProps.overScrollMode === WebViewOverScrollMode.NEVER) {
853
- this.overScrollMode = OverScrollMode.NEVER
854
- }
855
-
856
- this.overScrollMode = this.descriptorWrapper.rawProps.bounces ? OverScrollMode.ALWAYS : OverScrollMode.NEVER;
857
-
858
- if (this.descriptorWrapper.rawProps.injectedJavaScriptBeforeContentLoaded) {
859
- this.injectedJavaScriptBeforeContentLoaded = [
860
- { script: this.descriptorWrapper.rawProps.injectedJavaScriptBeforeContentLoaded, scriptRules: ["*"] }
861
- ];
862
- }
863
- }
864
-
865
- private registerPostMessage() {
866
- if (this.messagingEnabled == this.descriptorWrapper.rawProps.messagingEnabled) {
867
- return;
868
- }
869
- this.messagingEnabled = this.descriptorWrapper.rawProps.messagingEnabled;
870
- if (this.messagingEnabled) {
871
- let bridge: RNCWebViewBridge = {
872
- postMessage: (data: string) => {
873
- Logger.debug(TAG, `[RNOH] bridge postMessage, ${JSON.stringify(data)}`);
874
- if (this.controller != null) {
875
- let result: WebViewEventParams = this.createWebViewEvent("onMessage")
876
- if (result) {
877
- result.data = data.toString()
878
- result.lockIdentifier = this.webViewBaseOperate?.getLockIdentifier()
879
- this.eventEmitter!.emit("message", result as ResultType);
880
- }
881
- }
882
- }
883
- };
884
- this.controller.registerJavaScriptProxy(bridge, JAVASCRIPT_INTERFACE, ["postMessage"])
885
- this.source.uri ?
886
- this.controller.loadUrl(this.source.uri, this.headers) : this.controller.refresh()
887
- this.hasRegisterJavaScriptProxy = true
888
- }
889
- }
1
+ // Copyright (c) 2025 Huawei Device Co., Ltd. All rights reserved
2
+ // Use of this source code is governed by a Apache-2.0 license that can be
3
+ // found in the LICENSE file.
4
+
5
+ import { RNComponentContext } from '@rnoh/react-native-openharmony';
6
+ import webview from '@ohos.web.webview';
7
+ import { url as OSUrl } from '@kit.ArkTS';
8
+ import { RNC, TM } from './generated';
9
+ import Logger from './Logger';
10
+ import { BaseOperate } from './WebViewBaseOperate';
11
+ import { deviceInfo ,BusinessError} from '@kit.BasicServicesKit';
12
+ import {
13
+ AlertEvent,
14
+ CACHE_MODE,
15
+ JAVASCRIPT_INTERFACE,
16
+ ONE_HUNDRED,
17
+ EIGHT,
18
+ ResultType,
19
+ RNCWebViewBridge,
20
+ WebViewEventParams,
21
+ WebViewNewSource,
22
+ WebViewOverScrollMode,
23
+ ZERO,
24
+ COMMAND_NAME,
25
+ WebViewDescriptor,
26
+ REFRESH_OFFSET,
27
+ MINHEIGHT,
28
+ WebMediaCapturePermissionGrantType,
29
+ MenuItem
30
+ } from './Magic';
31
+
32
+ import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';
33
+ import { WebViewTurboModule, ShouldStartParams, ShouldOverrideCallbackState } from './WebViewTurboModule';
34
+ import { AnyThreadRNInstance, WorkerTaskRunnable } from '@rnoh/react-native-openharmony/src/main/ets/RNOH';
35
+ import buffer from '@ohos.buffer';
36
+
37
+ export const TAG = "WebView"
38
+
39
+ export const WEB_VIEW = "RNCWebView"
40
+
41
+ @Sendable
42
+ class WorkerRunnable implements WorkerTaskRunnable<ShouldStartParams> {
43
+ run(rnInstance: AnyThreadRNInstance, params: ShouldStartParams): void {
44
+ params.lockState = ShouldOverrideCallbackState.UNDECIDED
45
+ rnInstance.getTurboModule<WebViewTurboModule>(TM.RNCWebViewModule.NAME).setShouldStartParams(params)
46
+ }
47
+ }
48
+
49
+ let atManager = abilityAccessCtrl.createAtManager();
50
+
51
+ @Component
52
+ export struct RNCWebView {
53
+ public static readonly NAME = RNC.RNCWebView.NAME
54
+ ctx!: RNComponentContext
55
+ tag: number = ZERO
56
+ source: WebViewNewSource = {
57
+ uri: "",
58
+ method: "",
59
+ body: "",
60
+ html: "",
61
+ baseUrl: ""
62
+ }
63
+ html: string | undefined = "";
64
+ url: string | Resource = "";
65
+ body: string | undefined = "";
66
+ controller: webview.WebviewController = new webview.WebviewController();
67
+ javaScriptEnable: boolean = true;
68
+ allowFileAccess: boolean = true;
69
+ forceDark: boolean = true;
70
+ minFontSize: number = EIGHT;
71
+ overScrollMode: OverScrollMode = OverScrollMode.NEVER;
72
+ progress: number = ZERO;
73
+ cacheMode: number = CacheMode.Default;
74
+ requestUrl: string = "";
75
+ bundleName: string = "";
76
+ messagingEnabled: boolean = false;
77
+ hasRegisterJavaScriptProxy: boolean = false;
78
+ controllerAttached: boolean = false;
79
+ renderMode: RenderMode = RenderMode.SYNC_RENDER;
80
+ scrollEnabled = true;
81
+ @State nestedScroll: NestedScrollMode = NestedScrollMode.SELF_FIRST;
82
+ headers: Array<webview.WebHeader> = []
83
+ injectedJavaScriptBeforeContentLoaded: Array<ScriptItem> = [
84
+ { script: '', scriptRules: ["*"] }
85
+ ];
86
+ allowPageStartInProgress = true;
87
+ @State webviewWidth: number = ZERO
88
+ @State webviewHeight: number = ZERO
89
+ @State isRefreshing: boolean = false
90
+ @State mode: MixedMode = MixedMode.All;
91
+ private unregisterDescriptorChangesListener?: () => void = undefined
92
+ private cleanupCommandCallback?: () => void = undefined
93
+ private eventEmitter: RNC.RNCWebView.EventEmitter | undefined = undefined
94
+ private cleanUpCallbacks: (() => void)[] = []
95
+ private descriptorWrapper: WebViewDescriptor = Object() as WebViewDescriptor
96
+ private webViewBaseOperate: BaseOperate | null = null
97
+ private shouldInterceptLoad: boolean = true
98
+ private callLoadTimer: number = -1
99
+ static readonly SHOULD_OVERRIDE_URL_LOADING_TIMEOUT: number = 250
100
+ private debounceTimer: number = -1
101
+ static readonly DEBOUNCE_TIME: number = 100
102
+ private isFirstDebounce: boolean = true
103
+
104
+ aboutToAppear() {
105
+ try {
106
+ this.initVariable()
107
+ this.getBundleName()
108
+ this.url = this.source.uri as string;
109
+ this.onDescriptorWrapperChange(this.descriptorWrapper)
110
+ this.unregisterDescriptorChangesListener = this.ctx.descriptorRegistry.subscribeToDescriptorChanges(this.tag,
111
+ (newDescriptor) => {
112
+ this.onDescriptorWrapperChange(newDescriptor as WebViewDescriptor)
113
+ }
114
+ )
115
+ BaseOperate.setThirdPartyCookiesEnabled(this.descriptorWrapper.rawProps.thirdPartyCookiesEnabled)
116
+ webview.WebviewController.setWebDebuggingAccess(this.descriptorWrapper.rawProps.webviewDebuggingEnabled)
117
+ this.registerCommandCallback()
118
+ if (this.hasIncognito()) {
119
+ this.cacheMode = CacheMode.Online;
120
+ }
121
+ if (this.source.headers) {
122
+ this.source.headers.forEach(item => {
123
+ if (item.name && item.value) {
124
+ this.headers.push({ headerKey: item.name, headerValue: item.value })
125
+ }
126
+ })
127
+ }
128
+ if (this.descriptorWrapper.rawProps.geolocationEnabled) {
129
+ atManager.requestPermissionsFromUser(this.ctx.uiAbilityContext, ["ohos.permission.APPROXIMATELY_LOCATION"]).then((data) => {
130
+ }).catch((error: BusinessError) => {
131
+ console.error(`Failed to request permissions from user. Code is ${error.code}, message is ${error.message}`);
132
+ })
133
+ }
134
+ } catch (error) {
135
+ Logger.error(TAG, `[RNOH]Errorcode: ${error.code}, Message: ${error.message}`);
136
+ }
137
+
138
+ }
139
+
140
+ hasIncognito(): boolean {
141
+ return this.descriptorWrapper.rawProps.incognito && this.descriptorWrapper.rawProps.incognito === true
142
+ }
143
+
144
+ createWebViewEvent(type: string): WebViewEventParams {
145
+ return this.webViewBaseOperate?.createWebViewEvent({ type, progress: this.progress }) as WebViewEventParams;
146
+ }
147
+
148
+ aboutToDisappear() {
149
+ this.getUIContext().getFocusController().clearFocus()
150
+ this.cleanupCommandCallback?.()
151
+ this.unregisterDescriptorChangesListener?.()
152
+ this.cleanUpCallbacks.forEach(cb => cb())
153
+ try {
154
+ if (this.hasIncognito()) {
155
+ this.webViewBaseOperate?.setIncognito()
156
+ }
157
+ this.controller.deleteJavaScriptRegister(JAVASCRIPT_INTERFACE)
158
+ this.controller.refresh()
159
+ this.hasRegisterJavaScriptProxy = false
160
+ this.webViewBaseOperate = null
161
+ } catch (error) {
162
+ Logger.error(TAG, `[RNOH]Errorcode: ${error.code}, Message: ${error.message}`);
163
+ }
164
+ }
165
+
166
+ registerCommandCallback() {
167
+ this.cleanupCommandCallback = this.ctx.componentCommandReceiver.registerCommandCallback(
168
+ this.tag,
169
+ (command: COMMAND_NAME, args: string[]) => this.webViewBaseOperate?.registerCommandCallback(command, args));
170
+ }
171
+
172
+ onLoadingStart() {
173
+ this.webViewBaseOperate?.emitLoadingStart({ progress: this.progress })
174
+ }
175
+
176
+ onProgressChange(event: OnProgressChangeEvent) {
177
+ this.progress = event.newProgress
178
+ // 修复单页面应用切换路由时 onPageBegin 未触发的问题
179
+ if (this.controller.getUrl() !== this.url && this.progress < ONE_HUNDRED && this.allowPageStartInProgress) {
180
+ this.allowPageStartInProgress = false;
181
+ this.onLoadingStart();
182
+ }
183
+ this.webViewBaseOperate?.emitProgressChange({ progress: this.progress })
184
+ if (this.progress === ONE_HUNDRED) {
185
+ this.allowPageStartInProgress = true;
186
+ }
187
+ }
188
+
189
+ onLoadingFinish() {
190
+ this.webViewBaseOperate?.emitLoadingFinish({ progress: this.progress })
191
+ this.isRefreshing = false
192
+ }
193
+
194
+ runInjectedJavaScript() {
195
+ let injectedJS = this.descriptorWrapper.rawProps.injectedJavaScript
196
+ if (this.javaScriptEnable && injectedJS != "" && this.controllerAttached) {
197
+ try {
198
+ this.controller.runJavaScript("(function() {\n" + injectedJS + ";\n})();")
199
+ .then((result) => {
200
+ Logger.debug(TAG, '[RNOH] result: ' + result);
201
+ })
202
+ .catch((error: string | Error) => {
203
+ Logger.error(TAG, "[RNOH] error: " + error);
204
+ })
205
+ } catch (error) {
206
+ Logger.error(TAG, `[RNOH]Errorcode: ${error.code}, Message: ${error.message}`);
207
+ }
208
+ }
209
+ }
210
+
211
+ getJsDialogTitle() {
212
+ const url = this.controller.getUrl()
213
+ // 对于 data: URL,只显示 'JavaScript',类似于 Chrome
214
+ let title = 'JavaScript';
215
+ const isDataUrl = (url !== null) && url.startsWith("data:");
216
+
217
+
218
+ if (!isDataUrl) {
219
+ try {
220
+ const dialogUrl = OSUrl.URL.parseURL(url);
221
+ title = `网址为”${dialogUrl.protocol}//${dialogUrl.host}“的网页显示:`;
222
+ } catch (ex) {
223
+ title = url;
224
+ }
225
+ }
226
+
227
+ return title;
228
+ }
229
+
230
+ onJavascriptAlert(event?: AlertEvent) {
231
+ if (event) {
232
+ AlertDialog.show({
233
+ title: this.getJsDialogTitle(),
234
+ message: event.message,
235
+ alignment: DialogAlignment.Center,
236
+ secondaryButton: {
237
+ value: this.resourceToString($r('app.string.determined')),
238
+ action: () => event.result.handleConfirm()
239
+ },
240
+ cancel: () => event.result.handleCancel(),
241
+ })
242
+ }
243
+ return true
244
+ }
245
+
246
+ onJavascriptConfirm(event?: AlertEvent) {
247
+ if (event) {
248
+ AlertDialog.show({
249
+ title: this.getJsDialogTitle(),
250
+ message: event.message,
251
+ alignment: DialogAlignment.Center,
252
+ secondaryButton: {
253
+ value: this.resourceToString($r('app.string.determined')),
254
+ action: () => event.result.handleConfirm()
255
+ },
256
+ cancel: () => event.result.handleCancel(),
257
+ primaryButton: {
258
+ value: this.resourceToString($r('app.string.cancel')),
259
+ action: () => event.result.handleCancel()
260
+ },
261
+ })
262
+ }
263
+ return true
264
+ }
265
+
266
+ ignoreSilentHardwareSwitchMethods(ignoreSilentHardwareSwitch: boolean) {
267
+ this.webViewBaseOperate?.ignoreSilentHardwareSwitchMethods(ignoreSilentHardwareSwitch)
268
+ }
269
+
270
+ loadHtmlData(html: string = '', url: string = '') {
271
+ try {
272
+ this.controller.loadData(
273
+ html,
274
+ "text/html",
275
+ "UTF-8",
276
+ url,
277
+ " "
278
+ );
279
+ } catch (error) {
280
+ Logger.error(TAG, "error:" + error)
281
+ }
282
+ }
283
+
284
+ controllerAttachedInit(): void {
285
+ this.controllerAttached = true;
286
+ this.eventEmitter = new RNC.RNCWebView.EventEmitter(this.ctx.rnInstance, this.tag)
287
+ this.webViewBaseOperate = new BaseOperate(this.eventEmitter, this.controller)
288
+ this.webViewBaseOperate.setCustomUserAgent(this.descriptorWrapper.rawProps.userAgent,
289
+ this.descriptorWrapper.rawProps.applicationNameForUserAgent)
290
+ this.webViewBaseOperate.setFraudulentWebsiteWarningEnabled(this.descriptorWrapper.rawProps.fraudulentWebsiteWarningEnabled)
291
+
292
+ let baseUrl = this.source.baseUrl
293
+ let uri = this.source.uri
294
+ if (this.source.html != undefined && this.source.html != "") {
295
+ this.loadHtmlData(this.source.html, baseUrl)
296
+ } else if (uri != undefined && uri != "") {
297
+ if (this.source.method && this.source.method.toUpperCase() === 'POST' && this.source.body) {
298
+ let postData = buffer.from(this.source.body);
299
+ let url = uri as string;
300
+ this.controller.postUrl(url, postData?.buffer);
301
+ } else {
302
+ this.controller.loadUrl(uri, this.headers);
303
+ }
304
+ } else {
305
+ this.loadHtmlData()
306
+ }
307
+ if (!this.hasRegisterJavaScriptProxy) {
308
+ this.registerPostMessage()
309
+ }
310
+ }
311
+
312
+ onPageBegin() {
313
+ try {
314
+ if (this.controller.getUrl() === this.url) {
315
+ this.onLoadingStart();
316
+ }
317
+ this.controller.setScrollable(this.scrollEnabled)
318
+ this.ignoreSilentHardwareSwitchMethods(this.descriptorWrapper.rawProps.ignoreSilentHardwareSwitch)
319
+ } catch (error) {
320
+ Logger.debug(TAG,
321
+ `[RNOH] setignoreSilentHardwareSwitch and setScrollable ErrorCode: ${error.code}, Message: ${error.message}, userAgent: ${this.descriptorWrapper.rawProps.userAgent}`);
322
+ }
323
+ }
324
+
325
+ getBundleName() {
326
+ let bundleFlags =
327
+ bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_HAP_MODULE | bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_ABILITY |
328
+ bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION;
329
+
330
+ try {
331
+ bundleManager.getBundleInfoForSelf(bundleFlags, (error, data) => {
332
+ if (error) {
333
+ Logger.error(TAG,
334
+ `[RNOH] getBundleInfoForSelf failed ErrorCode: ${error.code}, Message: ${error.message}`);
335
+ } else {
336
+ this.bundleName = JSON.stringify(data.name)
337
+ }
338
+ });
339
+ } catch (error) {
340
+ Logger.error(TAG,
341
+ `[RNOH] getBundleInfoForSelf failed ErrorCode: ${error.code}, Message: ${error.message}`);
342
+ }
343
+ }
344
+
345
+ resourceToString(resource: Resource): string {
346
+ return getContext(this).resourceManager.getStringSync(resource)
347
+ }
348
+
349
+ onLoadFinished() {
350
+ this.onLoadingFinish()
351
+ }
352
+
353
+ onPageEndExecuteRunInjectedJavaScript(){
354
+ this.runInjectedJavaScript()
355
+ }
356
+
357
+ onPageEnd() {
358
+ this.runInjectedJavaScript()
359
+ this.onLoadingFinish()
360
+ }
361
+
362
+ getPermissionDialogMessage(event: OnPermissionRequestEvent) {
363
+ let permissionDialogMessage = ''
364
+ permissionDialogMessage = event.request.getAccessibleResource().toString()
365
+ switch (permissionDialogMessage) {
366
+ case ProtectedResourceType.VIDEO_CAPTURE:
367
+ permissionDialogMessage = this.resourceToString($r('app.string.use_your_camera'))
368
+ break;
369
+ case ProtectedResourceType.AUDIO_CAPTURE:
370
+ permissionDialogMessage = this.resourceToString($r('app.string.use_your_microphone'))
371
+ break;
372
+ case ProtectedResourceType.MidiSysex:
373
+ permissionDialogMessage = this.resourceToString($r('app.string.midi_sysex_resources'))
374
+ break;
375
+ case ProtectedResourceType.VIDEO_CAPTURE + "," + ProtectedResourceType.AUDIO_CAPTURE:
376
+ permissionDialogMessage = this.resourceToString($r('app.string.use_your_camera_and_microphone'))
377
+ break;
378
+ }
379
+ return permissionDialogMessage;
380
+ }
381
+ originWhitelistToRegex(originWhitelist: string): string {
382
+ const matchOperatorsRegex = /[|\\{}()[\]^$+*?.-]/g;
383
+ return `^${originWhitelist.replace(matchOperatorsRegex, '\\$&').replace(/\\\*/g, '.*')}`;
384
+ }
385
+ extractOrigin(url: string): string {
386
+ const result = /^[A-Za-z][A-Za-z0-9+\-.]+:(\/\/)?[^/]*/.exec(url);
387
+ return result === null ? '' : result[0];
388
+ };
389
+ passesWhitelist(compiledWhitelist: string[], url: string) {
390
+ const origin = this.extractOrigin(url);
391
+ return compiledWhitelist.some((x) => new RegExp(x).test(origin));
392
+ };
393
+ compileWhitelist(originWhitelist: string[]): string[] {
394
+ return ['about:blank', ...(originWhitelist || [])].map(this.originWhitelistToRegex);
395
+ }
396
+
397
+ onLoadIntercept(event: OnLoadInterceptEvent): boolean {
398
+ Logger.debug(TAG, `onLoadIntercept start`)
399
+ let originWhitelistArr = this.descriptorWrapper.rawProps.originWhitelist;
400
+ let hasOnShouldStartLoadWithRequestEventBool = this.descriptorWrapper.rawProps.hasOnShouldStartLoadWithRequestEvent
401
+ let urlString =
402
+ this.webViewBaseOperate?.verifyURLFormat(event.data.getRequestUrl(), this.source.html, this.source.baseUrl) || "";
403
+
404
+ if (this.passesWhitelist(this.compileWhitelist(originWhitelistArr), urlString) &&
405
+ !hasOnShouldStartLoadWithRequestEventBool) {
406
+ return false
407
+ }
408
+ this.webViewBaseOperate?.setLockIdentifier(BaseOperate.generateLockIdentifier())
409
+
410
+ const params = new ShouldStartParams();
411
+ params.lockIdentifier = this.webViewBaseOperate?.getLockIdentifier() || -1
412
+ this.ctx.runOnWorkerThread(new WorkerRunnable(), params);
413
+ this.webViewBaseOperate?.emitShouldStartLoadWithRequest(event, this.source.html, this.source.baseUrl)
414
+ const startTime = Date.now();
415
+ // 当lockState没变化并且小于超时时间,阻塞进程
416
+ while (params.lockState === ShouldOverrideCallbackState.UNDECIDED &&
417
+ Date.now() - startTime < RNCWebView.SHOULD_OVERRIDE_URL_LOADING_TIMEOUT) {
418
+ // 空循环等待
419
+ }
420
+
421
+ Logger.debug('params.lockState res:' + params.lockState + `, wait time is ${Date.now() - startTime}`);
422
+ return params.lockState === ShouldOverrideCallbackState.SHOULD_OVERRIDE;
423
+ }
424
+
425
+ onOverrideUrlLoading(event: WebResourceRequest) {
426
+ this.webViewBaseOperate?.emitShouldStartLoadWithRequestOverrideUrlLoading(event)
427
+ return false
428
+ }
429
+
430
+ onCreateMenu = (menuItems: Array<TextMenuItem>) => {
431
+ let customMenuItems = this.descriptorWrapper.rawProps.menuItems
432
+ if (customMenuItems == undefined) {
433
+ return menuItems;
434
+ } else if (customMenuItems.length == 0) {
435
+ return [];
436
+ } else {
437
+ let menuItemsArray: Array<TextMenuItem> = [];
438
+ for (let index = 0; index < customMenuItems.length; index++) {
439
+ let element: MenuItem = customMenuItems[index];
440
+ let customItem: TextMenuItem = {
441
+ content: element.label,
442
+ id: TextMenuItemId.of(element.key)
443
+ };
444
+ menuItemsArray.push(customItem)
445
+ }
446
+ return menuItemsArray;
447
+ }
448
+ }
449
+
450
+ onMenuItemClick(menuItem: TextMenuItem): boolean {
451
+ if (this.descriptorWrapper.rawProps.menuItems != undefined) {
452
+ this.controller.runJavaScript("(function(){return {selection: window.getSelection().toString()} })()")
453
+ .then((result: string) => {
454
+ this.webViewBaseOperate?.customMenuSelection(menuItem, result)
455
+ })
456
+ .catch((error: string | Error) => {
457
+ Logger.info('[RNOH] function onMenuItemClick error: ' + error);
458
+ })
459
+ // 禁用系统默认行为
460
+ return true;
461
+ }
462
+ return false;
463
+ }
464
+
465
+ onPermissionRequestAlert(event: OnPermissionRequestEvent){
466
+ AlertDialog.show({
467
+ title: this.resourceToString($r('app.string.on_confirm')) + event.request.getOrigin(),
468
+ message: this.getPermissionDialogMessage(event),
469
+ alignment: DialogAlignment.Center,
470
+ primaryButton: {
471
+ value: $r('app.string.deny'),
472
+ action: () => {
473
+ event.request.deny();
474
+ }
475
+ },
476
+ secondaryButton: {
477
+ value: $r('app.string.on_confirm'),
478
+ action: () => {
479
+ event.request.grant(event.request.getAccessibleResource());
480
+ }
481
+ },
482
+ cancel: () => {
483
+ event.request.deny();
484
+ }
485
+ })
486
+ }
487
+
488
+ async handleMediaCapturePermissionGrantType(resources: string[], domain: string) {
489
+ let permissionList: Array<Permissions> = [];
490
+ resources.forEach(resource => {
491
+ switch (resource.toString()) {
492
+ case 'TYPE_VIDEO_CAPTURE':
493
+ permissionList.push('ohos.permission.CAMERA');
494
+ break;
495
+ case 'TYPE_AUDIO_CAPTURE':
496
+ permissionList.push('ohos.permission.MICROPHONE');
497
+ break;
498
+ }
499
+ });
500
+
501
+ await atManager.requestPermissionsFromUser(this.ctx.uiAbilityContext, permissionList);
502
+ }
503
+
504
+ build() {
505
+ Stack() {
506
+ if ( deviceInfo.sdkApiVersion >= 20) {
507
+ Web({ src: "", controller: this.controller, renderMode: this.renderMode })
508
+ .mixedMode(this.mode)
509
+ .onPermissionRequest(async (event: OnPermissionRequestEvent) => {
510
+ if (event && (this.getPermissionDialogMessage(event) != "TYPE_SENSOR")) {
511
+ const resources: string[] = event.request.getAccessibleResource();
512
+ if (resources.indexOf('TYPE_AUDIO_CAPTURE') >= 0 || resources.indexOf('TYPE_VIDEO_CAPTURE') >= 0 ) {
513
+ const originUrl = this.webViewBaseOperate?.verifyURLFormat(event.request.getOrigin(), this.source.html,
514
+ this.source.baseUrl);
515
+ const currentUrl = this.webViewBaseOperate?.verifyURLFormat(this.controller.getUrl(), this.source.html,
516
+ this.source.baseUrl);
517
+ const originDomain: string = OSUrl.URL.parseURL(originUrl).hostname;
518
+ const currentDomain: string = OSUrl.URL.parseURL(currentUrl).hostname;
519
+ let mediaCapturePermissionGrantType: string = WebMediaCapturePermissionGrantType.PROMPT
520
+ if (this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType != undefined) {
521
+ mediaCapturePermissionGrantType = this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType;
522
+ }
523
+ if (mediaCapturePermissionGrantType != WebMediaCapturePermissionGrantType.DENY) {
524
+ await this.handleMediaCapturePermissionGrantType(resources, originDomain)
525
+ }
526
+ switch (mediaCapturePermissionGrantType) {
527
+ case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_PROMPT:
528
+ if (originDomain != currentDomain) {
529
+ this.onPermissionRequestAlert(event)
530
+ } else {
531
+ event.request.grant(resources);
532
+ }
533
+ break;
534
+ case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_DENY:
535
+ if (originDomain != currentDomain) {
536
+ event.request.deny();
537
+ } else {
538
+ event.request.grant(resources);
539
+ }
540
+ break;
541
+ case WebMediaCapturePermissionGrantType.DENY:
542
+ event.request.deny();
543
+ break;
544
+ case WebMediaCapturePermissionGrantType.GRANT:
545
+ event.request.grant(resources);
546
+ break;
547
+ case WebMediaCapturePermissionGrantType.PROMPT:
548
+ this.onPermissionRequestAlert(event)
549
+ break;
550
+ }
551
+ } else {
552
+ this.onPermissionRequestAlert(event)
553
+ }
554
+ }
555
+ })
556
+ .width(this.webviewWidth)
557
+ .height(this.webviewHeight)
558
+ .fileAccess(this.allowFileAccess)
559
+ .constraintSize({ minHeight: MINHEIGHT })
560
+ .overScrollMode(this.overScrollMode)
561
+ .javaScriptAccess(this.javaScriptEnable)
562
+ .javaScriptOnDocumentStart(this.injectedJavaScriptBeforeContentLoaded)
563
+ .horizontalScrollBarAccess(this.descriptorWrapper.rawProps.showsHorizontalScrollIndicator)
564
+ .verticalScrollBarAccess(this.descriptorWrapper.rawProps.showsVerticalScrollIndicator)
565
+ .overviewModeAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
566
+ .textZoomRatio(this.descriptorWrapper.rawProps.textZoom)
567
+ .backgroundColor(BaseOperate.getColorMode(this.ctx.uiAbilityContext,
568
+ this.descriptorWrapper.rawProps.forceDarkOn) === WebDarkMode.On ? Color.White : Color.Transparent)
569
+ .darkMode(BaseOperate.getColorMode(this.ctx.uiAbilityContext, this.descriptorWrapper.rawProps.forceDarkOn))
570
+ .forceDarkAccess(this.forceDark)
571
+ .cacheMode(this.cacheMode)
572
+ .minFontSize(this.minFontSize)
573
+ .domStorageAccess(this.descriptorWrapper.rawProps.domStorageEnabled)
574
+ .zoomAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
575
+ .overScrollMode(this.overScrollMode)
576
+ .onProgressChange((event: OnProgressChangeEvent) => this.onProgressChange(event))
577
+ .onScroll((event: OnScrollEvent) => this.webViewBaseOperate?.emitScroll(event))
578
+ .nestedScroll({
579
+ scrollForward: this.nestedScroll,
580
+ scrollBackward: this.nestedScroll,
581
+ })
582
+ .onLoadStarted(() => this.onPageBegin())
583
+ .onLoadFinished(() => this.onLoadFinished())
584
+ .onPageEnd(() => {this.onPageEndExecuteRunInjectedJavaScript()})
585
+ .onErrorReceive((event: OnErrorReceiveEvent) => this.webViewBaseOperate?.emitLoadingError(event))
586
+ .onHttpErrorReceive((event: OnHttpErrorReceiveEvent) => this.webViewBaseOperate?.emitHttpError(event))
587
+ .onControllerAttached(() => this.controllerAttachedInit())
588
+ .onAlert((event: AlertEvent) => this.onJavascriptAlert(event))
589
+ .onConfirm((event: AlertEvent) => this.onJavascriptConfirm(event))
590
+ .onDownloadStart((event: OnDownloadStartEvent) => this.webViewBaseOperate?.onDownloadStart(event))
591
+ .geolocationAccess(this.descriptorWrapper.rawProps.geolocationEnabled)
592
+ .onGeolocationShow((event) => {
593
+ if (event && (this.descriptorWrapper.rawProps.geolocationEnabled != undefined)) {
594
+ event.geolocation.invoke(event.origin, this.descriptorWrapper.rawProps.geolocationEnabled, true);
595
+ }
596
+ })
597
+ .mediaPlayGestureAccess(this.descriptorWrapper.rawProps.mediaPlaybackRequiresUserAction)
598
+ .onRenderExited((event: OnRenderExitedEvent) => this.webViewBaseOperate?.onRenderExited(event))
599
+ .onLoadIntercept((event: OnLoadInterceptEvent) => this.onLoadIntercept(event))
600
+ .onTitleReceive((event: OnTitleReceiveEvent) => this.webViewBaseOperate?.onTitleReceive(event))
601
+ .editMenuOptions({
602
+ onCreateMenu: this.onCreateMenu.bind(this),
603
+ onMenuItemClick: this.onMenuItemClick.bind(this),
604
+ })
605
+ } else {
606
+ Web({ src: "", controller: this.controller, renderMode: this.renderMode })
607
+ .mixedMode(this.mode)
608
+ .onPermissionRequest(async (event: OnPermissionRequestEvent) => {
609
+ if (event && (this.getPermissionDialogMessage(event) != "TYPE_SENSOR")) {
610
+ const resources: string[] = event.request.getAccessibleResource();
611
+ if (resources.indexOf('TYPE_AUDIO_CAPTURE') >= 0 || resources.indexOf('TYPE_VIDEO_CAPTURE') >= 0 ) {
612
+ const originUrl = this.webViewBaseOperate?.verifyURLFormat(event.request.getOrigin(), this.source.html,
613
+ this.source.baseUrl);
614
+ const currentUrl = this.webViewBaseOperate?.verifyURLFormat(this.controller.getUrl(), this.source.html,
615
+ this.source.baseUrl);
616
+ const originDomain: string = OSUrl.URL.parseURL(originUrl).hostname;
617
+ const currentDomain: string = OSUrl.URL.parseURL(currentUrl).hostname;
618
+ let mediaCapturePermissionGrantType: string = WebMediaCapturePermissionGrantType.PROMPT
619
+ if (this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType != undefined) {
620
+ mediaCapturePermissionGrantType = this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType;
621
+ }
622
+ if (mediaCapturePermissionGrantType != WebMediaCapturePermissionGrantType.DENY) {
623
+ await this.handleMediaCapturePermissionGrantType(resources, originDomain)
624
+ }
625
+ switch (mediaCapturePermissionGrantType) {
626
+ case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_PROMPT:
627
+ if (originDomain != currentDomain) {
628
+ this.onPermissionRequestAlert(event)
629
+ } else {
630
+ event.request.grant(resources);
631
+ }
632
+ break;
633
+ case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_DENY:
634
+ if (originDomain != currentDomain) {
635
+ event.request.deny();
636
+ } else {
637
+ event.request.grant(resources);
638
+ }
639
+ break;
640
+ case WebMediaCapturePermissionGrantType.DENY:
641
+ event.request.deny();
642
+ break;
643
+ case WebMediaCapturePermissionGrantType.GRANT:
644
+ event.request.grant(resources);
645
+ break;
646
+ case WebMediaCapturePermissionGrantType.PROMPT:
647
+ this.onPermissionRequestAlert(event)
648
+ break;
649
+ }
650
+ } else {
651
+ this.onPermissionRequestAlert(event)
652
+ }
653
+ }
654
+ })
655
+
656
+ .width(this.webviewWidth)
657
+ .height(this.webviewHeight)
658
+ .fileAccess(this.allowFileAccess)
659
+ .constraintSize({ minHeight: MINHEIGHT })
660
+ .overScrollMode(this.overScrollMode)
661
+ .javaScriptAccess(this.javaScriptEnable)
662
+ .javaScriptOnDocumentStart(this.injectedJavaScriptBeforeContentLoaded)
663
+ .horizontalScrollBarAccess(this.descriptorWrapper.rawProps.showsHorizontalScrollIndicator)
664
+ .verticalScrollBarAccess(this.descriptorWrapper.rawProps.showsVerticalScrollIndicator)
665
+ .overviewModeAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
666
+ .textZoomRatio(this.descriptorWrapper.rawProps.textZoom)
667
+ .backgroundColor(BaseOperate.getColorMode(this.ctx.uiAbilityContext,
668
+ this.descriptorWrapper.rawProps.forceDarkOn) === WebDarkMode.On ? Color.White : Color.Transparent)
669
+ .darkMode(BaseOperate.getColorMode(this.ctx.uiAbilityContext, this.descriptorWrapper.rawProps.forceDarkOn))
670
+ .forceDarkAccess(this.forceDark)
671
+ .cacheMode(this.cacheMode)
672
+ .minFontSize(this.minFontSize)
673
+ .domStorageAccess(this.descriptorWrapper.rawProps.domStorageEnabled)
674
+ .zoomAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
675
+ .overScrollMode(this.overScrollMode)
676
+ .onProgressChange((event: OnProgressChangeEvent) => this.onProgressChange(event))
677
+ .onScroll((event: OnScrollEvent) => this.webViewBaseOperate?.emitScroll(event))
678
+ .nestedScroll({
679
+ scrollForward: this.nestedScroll,
680
+ scrollBackward: this.nestedScroll,
681
+ })
682
+ .onPageBegin(() => this.onPageBegin())
683
+ .onPageEnd(() => this.onPageEnd())
684
+ .onErrorReceive((event: OnErrorReceiveEvent) => this.webViewBaseOperate?.emitLoadingError(event))
685
+ .onHttpErrorReceive((event: OnHttpErrorReceiveEvent) => this.webViewBaseOperate?.emitHttpError(event))
686
+ .onControllerAttached(() => this.controllerAttachedInit())
687
+ .onAlert((event: AlertEvent) => this.onJavascriptAlert(event))
688
+ .onConfirm((event: AlertEvent) => this.onJavascriptConfirm(event))
689
+ .onDownloadStart((event: OnDownloadStartEvent) => this.webViewBaseOperate?.onDownloadStart(event))
690
+ .geolocationAccess(this.descriptorWrapper.rawProps.geolocationEnabled)
691
+ .onGeolocationShow((event) => {
692
+ if (event && (this.descriptorWrapper.rawProps.geolocationEnabled != undefined)) {
693
+ event.geolocation.invoke(event.origin, this.descriptorWrapper.rawProps.geolocationEnabled, true);
694
+ }
695
+ })
696
+ .mediaPlayGestureAccess(this.descriptorWrapper.rawProps.mediaPlaybackRequiresUserAction)
697
+ .onRenderExited((event: OnRenderExitedEvent) => this.webViewBaseOperate?.onRenderExited(event))
698
+ .onLoadIntercept((event: OnLoadInterceptEvent) => this.onLoadIntercept(event))
699
+ .onTitleReceive((event: OnTitleReceiveEvent) => this.webViewBaseOperate?.onTitleReceive(event))
700
+ .editMenuOptions({
701
+ onCreateMenu: this.onCreateMenu.bind(this),
702
+ onMenuItemClick: this.onMenuItemClick.bind(this),
703
+ })
704
+ }
705
+ }
706
+ .width(this.webviewWidth)
707
+ .height(this.webviewHeight)
708
+ .position({
709
+ x: this.descriptorWrapper.layoutMetrics.frame.origin.x,
710
+ y: this.descriptorWrapper.layoutMetrics.frame.origin.y
711
+ })
712
+ }
713
+
714
+ private onDescriptorWrapperChange(descriptorWrapper: WebViewDescriptor) {
715
+ this.initVariable()
716
+ this.descriptorWrapper = descriptorWrapper
717
+ if (this.source.html != "" && this.html != this.source.html) {
718
+ if (this.controllerAttached) {
719
+ this.loadHtmlData(this.source.html, this.source.baseUrl)
720
+ Logger.debug(TAG, "[RNOH] html is update")
721
+ this.html = this.source.html
722
+ }
723
+ } else if (this.source.uri && this.source.method && this.source.method.toUpperCase() === 'POST' && this.source.body && (this.source.uri != this.url || this.body != this.source.body)) {
724
+ if (this.controllerAttached) {
725
+ let url = this.source.uri as string;
726
+ this.url = url;
727
+ this.body = this.source.body;
728
+ let postData = buffer.from(this.source.body);
729
+ this.controller.postUrl(url, postData?.buffer);
730
+ }
731
+ } else if (typeof this.url !== typeof this.source.uri
732
+ || (typeof this.source.uri === "string" && this.url !== this.source.uri)
733
+ || (typeof this.source.uri === "object" && (this.url as Resource)?.params !== (this.source.uri as Resource)?.params)) {
734
+ if (this.source.uri != "") {
735
+ Logger.debug(TAG, `[RNOH] newDescriptor props update uri: ` + this.source.uri);
736
+ if (this.controllerAttached) {
737
+ this.controller.loadUrl(this.source.uri, this.headers)
738
+ }
739
+ } else {
740
+ this.loadHtmlData()
741
+ }
742
+ this.url = this.source.uri as string;
743
+ }
744
+ if (this.controllerAttached) {
745
+ this.controller?.setScrollable(this.descriptorWrapper.rawProps.scrollEnabled)
746
+ }
747
+ }
748
+
749
+ private initVariable() {
750
+ this.descriptorWrapper = this.ctx.descriptorRegistry.getDescriptor<WebViewDescriptor>(this.tag)
751
+ this.javaScriptEnable = this.descriptorWrapper.rawProps.javaScriptEnabled;
752
+ this.minFontSize =
753
+ this.descriptorWrapper.rawProps.minimumFontSize ? this.descriptorWrapper.rawProps.minimumFontSize : EIGHT;
754
+ this.cacheMode =
755
+ this.descriptorWrapper.rawProps.cacheEnabled ?
756
+ this.webViewBaseOperate?.transCacheMode(this.descriptorWrapper.rawProps.cacheMode as CACHE_MODE) as number :
757
+ CacheMode.Online;
758
+ this.source = {
759
+ uri: this.descriptorWrapper.rawProps.newSource.uri ?? "",
760
+ method: this.descriptorWrapper.rawProps.newSource.method ?? "",
761
+ body: this.descriptorWrapper.rawProps.newSource.body ?? "",
762
+ html: this.descriptorWrapper.rawProps.newSource.html ?? "",
763
+ baseUrl: this.descriptorWrapper.rawProps.newSource.baseUrl ?? "",
764
+ headers: this.descriptorWrapper.rawProps.newSource.headers ?? []
765
+ }
766
+ if (this.source.headers) {
767
+ this.source.headers.forEach(item => {
768
+ if (item.name && item.value) {
769
+ this.headers.push({ headerKey: item.name, headerValue: item.value })
770
+ }
771
+ })
772
+ }
773
+ // this.html = this.source.html
774
+ const newWidth = this.descriptorWrapper.layoutMetrics.frame.size.width
775
+ const newHeight = this.descriptorWrapper.layoutMetrics.frame.size.height
776
+
777
+ if (this.debounceTimer !== -1) {
778
+ clearTimeout(this.debounceTimer)
779
+ this.debounceTimer = -1
780
+ }
781
+
782
+ if (this.isFirstDebounce) {
783
+ this.webviewWidth = newWidth
784
+ this.webviewHeight = newHeight
785
+ this.isFirstDebounce = false
786
+ } else {
787
+ this.debounceTimer = setTimeout(() => {
788
+ this.webviewWidth = newWidth
789
+ this.webviewHeight = newHeight
790
+ }, RNCWebView.DEBOUNCE_TIME)
791
+ }
792
+ this.scrollEnabled = this.descriptorWrapper.rawProps.scrollEnabled;
793
+ // 当禁止滚动时,使用父组件滚动
794
+ this.nestedScroll = this.scrollEnabled ? NestedScrollMode.SELF_FIRST : NestedScrollMode.PARENT_FIRST;
795
+ if (this.source.uri && this.source.uri.toString().startsWith("asset://")) {
796
+ this.source.uri = $rawfile(this.source.uri.toString().replace("asset://", this.ctx.rnInstance.getAssetsDest()));
797
+ }
798
+
799
+ if (this.descriptorWrapper.rawProps.overScrollMode === WebViewOverScrollMode.ALWAYS) {
800
+ this.overScrollMode = OverScrollMode.ALWAYS
801
+ } else if (this.descriptorWrapper.rawProps.overScrollMode === WebViewOverScrollMode.NEVER) {
802
+ this.overScrollMode = OverScrollMode.NEVER
803
+ }
804
+
805
+ this.overScrollMode = this.descriptorWrapper.rawProps.bounces ? OverScrollMode.ALWAYS : OverScrollMode.NEVER;
806
+
807
+ if (this.descriptorWrapper.rawProps.injectedJavaScriptBeforeContentLoaded) {
808
+ this.injectedJavaScriptBeforeContentLoaded = [
809
+ { script: this.descriptorWrapper.rawProps.injectedJavaScriptBeforeContentLoaded, scriptRules: ["*"] }
810
+ ];
811
+ }
812
+
813
+ const renderModeStr = this.descriptorWrapper.rawProps.renderMode || "SYNC_RENDER"
814
+ this.renderMode = (renderModeStr === "ASYNC_RENDER") ? RenderMode.ASYNC_RENDER : RenderMode.SYNC_RENDER
815
+
816
+ }
817
+
818
+ private registerPostMessage() {
819
+ if (this.messagingEnabled == this.descriptorWrapper.rawProps.messagingEnabled) {
820
+ return;
821
+ }
822
+ this.messagingEnabled = this.descriptorWrapper.rawProps.messagingEnabled;
823
+ if (this.messagingEnabled) {
824
+ let bridge: RNCWebViewBridge = {
825
+ postMessage: (data: string) => {
826
+ Logger.debug(TAG, `[RNOH] bridge postMessage, ${JSON.stringify(data)}`);
827
+ if (this.controller != null) {
828
+ let result: WebViewEventParams = this.createWebViewEvent("onMessage")
829
+ if (result) {
830
+ result.data = data.toString()
831
+ result.lockIdentifier = this.webViewBaseOperate?.getLockIdentifier()
832
+ this.eventEmitter!.emit("message", result as ResultType);
833
+ }
834
+ }
835
+ }
836
+ };
837
+ this.controller.registerJavaScriptProxy(bridge, JAVASCRIPT_INTERFACE, ["postMessage"])
838
+ this.source.uri ?
839
+ this.controller.loadUrl(this.source.uri, this.headers) : this.controller.refresh()
840
+ this.hasRegisterJavaScriptProxy = true
841
+ }
842
+ }
890
843
  }