diff --git a/.eslintrc.json b/.eslintrc.json
index 155eafd..3ad9327 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,12 +1,12 @@
{
"root": true,
"ignorePatterns": ["**/*"],
- "plugins": ["@nrwl/nx"],
+ "plugins": ["@nx"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
- "@nrwl/nx/enforce-module-boundaries": [
+ "@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
@@ -23,12 +23,12 @@
},
{
"files": ["*.ts", "*.tsx"],
- "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "plugin:@nrwl/nx/typescript"],
+ "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "plugin:@nx/typescript"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
- "extends": ["plugin:@nrwl/nx/javascript"],
+ "extends": ["plugin:@nx/javascript"],
"rules": {}
},
{
diff --git a/.swp b/.swp
index 8c81919..01cd552 100644
Binary files a/.swp and b/.swp differ
diff --git a/README.md b/README.md
index 9e9617e..dfe1722 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,7 @@
- [@essent/nativescript-medallia](packages/nativescript-medallia/README.md)
- [@essent/nativescript-ng-sentry](packages/nativescript-ng-sentry/README.md)
- [@essent/nativescript-urban-airship](packages/nativescript-urban-airship/README.md)
+- [@essent/nativescript-webview-ext](packages/nativescript-webview-ext/README.md)
# How to use?
diff --git a/apps/demo-angular/package.json b/apps/demo-angular/package.json
index b6c7700..944274d 100644
--- a/apps/demo-angular/package.json
+++ b/apps/demo-angular/package.json
@@ -7,7 +7,8 @@
"@essent/nativescript-appdynamics": "file:../../dist/packages/nativescript-appdynamics",
"@essent/nativescript-iadvize": "file:../../dist/packages/nativescript-iadvize",
"@essent/nativescript-medallia": "file:../../dist/packages/nativescript-medallia",
- "@essent/nativescript-urban-airship": "file:../../dist/packages/nativescript-urban-airship"
+ "@essent/nativescript-urban-airship": "file:../../dist/packages/nativescript-urban-airship",
+ "@essent/nativescript-webview-ext": "file:../../dist/packages/nativescript-webview-ext"
},
"devDependencies": {
"@nativescript/android": "~8.5.0",
diff --git a/apps/demo-angular/project.json b/apps/demo-angular/project.json
index 85bfd64..c960aa3 100644
--- a/apps/demo-angular/project.json
+++ b/apps/demo-angular/project.json
@@ -17,7 +17,7 @@
"dependsOn": [
{
"target": "build.all",
- "projects": "dependencies"
+ "dependencies": true
}
]
},
@@ -29,7 +29,7 @@
"dependsOn": [
{
"target": "build.demo",
- "projects": "dependencies"
+ "dependencies": true
}
]
},
@@ -41,7 +41,7 @@
"dependsOn": [
{
"target": "build.demo",
- "projects": "dependencies"
+ "dependencies": true
}
]
},
@@ -52,7 +52,7 @@
}
},
"lint": {
- "executor": "@nrwl/linter:eslint",
+ "executor": "@nx/linter:eslint",
"options": {
"lintFilePatterns": ["apps/demo-angular/**/*.ts"]
}
diff --git a/apps/demo-angular/src/app-routing.module.ts b/apps/demo-angular/src/app-routing.module.ts
index 0d7cd81..2aac7fe 100644
--- a/apps/demo-angular/src/app-routing.module.ts
+++ b/apps/demo-angular/src/app-routing.module.ts
@@ -13,6 +13,7 @@ const routes: Routes = [
{ path: 'nativescript-medallia', loadChildren: () => import('./plugin-demos/nativescript-medallia.module').then((m) => m.NativescriptMedalliaModule) },
{ path: 'nativescript-ng-sentry', loadChildren: () => import('./plugin-demos/nativescript-ng-sentry.module').then((m) => m.NativescriptNgSentryModule) },
{ path: 'nativescript-urban-airship', loadChildren: () => import('./plugin-demos/nativescript-urban-airship.module').then((m) => m.NativescriptUrbanAirshipModule) },
+ { path: 'nativescript-webview-ext', loadChildren: () => import('./plugin-demos/nativescript-webview-ext.module').then((m) => m.NativescriptWebviewExtModule) },
];
@NgModule({
diff --git a/apps/demo-angular/src/home.component.ts b/apps/demo-angular/src/home.component.ts
index 15e0a2b..ad05f59 100644
--- a/apps/demo-angular/src/home.component.ts
+++ b/apps/demo-angular/src/home.component.ts
@@ -24,5 +24,8 @@ export class HomeComponent {
{
name: 'nativescript-urban-airship',
},
+ {
+ name: 'nativescript-webview-ext',
+ },
];
}
diff --git a/apps/demo-angular/src/plugin-demos/nativescript-webview-ext.component.html b/apps/demo-angular/src/plugin-demos/nativescript-webview-ext.component.html
new file mode 100644
index 0000000..d805f65
--- /dev/null
+++ b/apps/demo-angular/src/plugin-demos/nativescript-webview-ext.component.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/apps/demo-angular/src/plugin-demos/nativescript-webview-ext.component.ts b/apps/demo-angular/src/plugin-demos/nativescript-webview-ext.component.ts
new file mode 100644
index 0000000..0402833
--- /dev/null
+++ b/apps/demo-angular/src/plugin-demos/nativescript-webview-ext.component.ts
@@ -0,0 +1,17 @@
+import { Component, NgZone } from '@angular/core';
+import { DemoSharedNativescriptWebviewExt } from '@demo/shared';
+import {} from '@essent/nativescript-webview-ext';
+
+@Component({
+ selector: 'demo-nativescript-webview-ext',
+ templateUrl: 'nativescript-webview-ext.component.html',
+})
+export class NativescriptWebviewExtComponent {
+ demoShared: DemoSharedNativescriptWebviewExt;
+
+ constructor(private _ngZone: NgZone) {}
+
+ ngOnInit() {
+ this.demoShared = new DemoSharedNativescriptWebviewExt();
+ }
+}
diff --git a/apps/demo-angular/src/plugin-demos/nativescript-webview-ext.module.ts b/apps/demo-angular/src/plugin-demos/nativescript-webview-ext.module.ts
new file mode 100644
index 0000000..828b42a
--- /dev/null
+++ b/apps/demo-angular/src/plugin-demos/nativescript-webview-ext.module.ts
@@ -0,0 +1,10 @@
+import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
+import { NativeScriptCommonModule, NativeScriptRouterModule } from '@nativescript/angular';
+import { NativescriptWebviewExtComponent } from './nativescript-webview-ext.component';
+
+@NgModule({
+ imports: [NativeScriptCommonModule, NativeScriptRouterModule.forChild([{ path: '', component: NativescriptWebviewExtComponent }])],
+ declarations: [NativescriptWebviewExtComponent],
+ schemas: [NO_ERRORS_SCHEMA],
+})
+export class NativescriptWebviewExtModule {}
diff --git a/apps/demo/package.json b/apps/demo/package.json
index 018db4f..3e26c2a 100644
--- a/apps/demo/package.json
+++ b/apps/demo/package.json
@@ -10,7 +10,8 @@
"@essent/nativescript-appdynamics": "file:../../packages/nativescript-appdynamics",
"@essent/nativescript-iadvize": "file:../../packages/nativescript-iadvize",
"@essent/nativescript-medallia": "file:../../packages/nativescript-medallia",
- "@essent/nativescript-urban-airship": "file:../../packages/nativescript-urban-airship"
+ "@essent/nativescript-urban-airship": "file:../../packages/nativescript-urban-airship",
+ "@essent/nativescript-webview-ext": "file:../../packages/nativescript-webview-ext"
},
"devDependencies": {
"@nativescript/android": "~8.5.0",
diff --git a/apps/demo/project.json b/apps/demo/project.json
index 87572e6..005d2aa 100644
--- a/apps/demo/project.json
+++ b/apps/demo/project.json
@@ -17,7 +17,7 @@
"dependsOn": [
{
"target": "build.all",
- "projects": "dependencies"
+ "dependencies": true
}
]
},
@@ -29,7 +29,7 @@
"dependsOn": [
{
"target": "build.demo",
- "projects": "dependencies"
+ "dependencies": true
}
]
},
@@ -41,7 +41,7 @@
"dependsOn": [
{
"target": "build.demo",
- "projects": "dependencies"
+ "dependencies": true
}
]
},
@@ -52,7 +52,7 @@
}
},
"lint": {
- "executor": "@nrwl/linter:eslint",
+ "executor": "@nx/linter:eslint",
"options": {
"lintFilePatterns": ["apps/demo/**/*.ts"]
}
diff --git a/apps/demo/src/main-page.xml b/apps/demo/src/main-page.xml
index 098929c..e3491fa 100644
--- a/apps/demo/src/main-page.xml
+++ b/apps/demo/src/main-page.xml
@@ -11,6 +11,7 @@
+
diff --git a/apps/demo/src/plugin-demos/nativescript-webview-ext.ts b/apps/demo/src/plugin-demos/nativescript-webview-ext.ts
new file mode 100644
index 0000000..c28ccaf
--- /dev/null
+++ b/apps/demo/src/plugin-demos/nativescript-webview-ext.ts
@@ -0,0 +1,10 @@
+import { Observable, EventData, Page } from '@nativescript/core';
+import { DemoSharedNativescriptWebviewExt } from '@demo/shared';
+import {} from '@essent/nativescript-webview-ext';
+
+export function navigatingTo(args: EventData) {
+ const page = args.object;
+ page.bindingContext = new DemoModel();
+}
+
+export class DemoModel extends DemoSharedNativescriptWebviewExt {}
diff --git a/apps/demo/src/plugin-demos/nativescript-webview-ext.xml b/apps/demo/src/plugin-demos/nativescript-webview-ext.xml
new file mode 100644
index 0000000..5fd4387
--- /dev/null
+++ b/apps/demo/src/plugin-demos/nativescript-webview-ext.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/jest.config.ts b/jest.config.ts
index 3eeb77b..5f64584 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -1,3 +1,3 @@
-const { getJestProjects } = require('@nrwl/jest');
+const { getJestProjects } = require('@nx/jest');
export default { projects: [...getJestProjects()] };
diff --git a/migrations.json b/migrations.json
deleted file mode 100644
index 9ac2c8c..0000000
--- a/migrations.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "migrations": [
- {
- "cli": "nx",
- "version": "5.0.2",
- "description": "Migrate tools to 5.0.0",
- "implementation": "./src/migrations/update-5-0-0/update-5-0-0",
- "package": "@nativescript/plugin-tools",
- "name": "update-to-5.0.2"
- },
- {
- "cli": "nx",
- "version": "5.1.0",
- "description": "Migrate tools to 5.1.0",
- "implementation": "./src/migrations/update-5-1-0/update-5-1-0",
- "package": "@nativescript/plugin-tools",
- "name": "update-to-5.1.0"
- }
- ]
-}
diff --git a/package.json b/package.json
index 5badf59..ffe083c 100644
--- a/package.json
+++ b/package.json
@@ -16,31 +16,37 @@
},
"private": true,
"devDependencies": {
- "@angular/animations": "^15.0.0",
- "@angular/common": "^15.0.0",
- "@angular/compiler": "^15.0.0",
- "@angular/compiler-cli": "^15.0.0",
- "@angular/core": "^15.0.0",
- "@angular/forms": "^15.0.0",
- "@angular/platform-browser": "^15.0.0",
- "@angular/platform-browser-dynamic": "^15.0.0",
- "@angular/router": "^15.0.0",
- "@nativescript/angular": "^15.0.0",
+ "@angular-devkit/build-angular": "^16.0.0",
+ "@angular/animations": "~16.0.0",
+ "@angular/common": "~16.0.0",
+ "@angular/compiler": "~16.0.0",
+ "@angular/compiler-cli": "~16.0.0",
+ "@angular/core": "~16.0.0",
+ "@angular/forms": "~16.0.0",
+ "@angular/platform-browser": "~16.0.0",
+ "@angular/platform-browser-dynamic": "~16.0.0",
+ "@angular/router": "~16.0.0",
+ "@nativescript/angular": "^16.0.0",
"@nativescript/appversion": "^2.0.0",
"@nativescript/core": "~8.5.0",
- "@nativescript/plugin-tools": "5.1.0",
+ "@nativescript/plugin-tools": "5.2.0-alpha.1",
"@nativescript/types": "~8.5.0",
"@nativescript/webpack": "~5.0.5",
- "@ngtools/webpack": "^15.0.0",
+ "@ngtools/webpack": "~16.0.0",
"husky": "^8.0.0",
"moment": "^2.0.0",
"nativescript-vue": "~2.9.0",
"nativescript-vue-template-compiler": "~2.9.0",
- "ng-packagr": "^15.0.0",
+ "ng-packagr": "~16.0.0",
+ "promise-polyfill": "~8.3.0",
"rxjs": "~7.5.0",
- "typescript": "~4.8.0",
- "zone.js": "~0.13.0",
- "@angular-devkit/build-angular": "^15.0.0"
+ "ts-patch": "^3.0.0",
+ "typescript": "~5.0.0",
+ "whatwg-fetch": "^3.6.2",
+ "zone.js": "~0.13.0"
+ },
+ "resolutions": {
+ "ts-patch": "^3.0.0"
},
"lint-staged": {
"**/*.{js,ts,scss,json,html}": [
diff --git a/packages/nativescript-adobe-experience-cloud/platforms/android/nativescript_adobe_experience_cloud.aar b/packages/nativescript-adobe-experience-cloud/platforms/android/nativescript_adobe_experience_cloud.aar
index dfbdc75..6ee8c7f 100644
Binary files a/packages/nativescript-adobe-experience-cloud/platforms/android/nativescript_adobe_experience_cloud.aar and b/packages/nativescript-adobe-experience-cloud/platforms/android/nativescript_adobe_experience_cloud.aar differ
diff --git a/packages/nativescript-adobe-experience-cloud/platforms/android/nativescript_adobe_marketing_cloud.aar b/packages/nativescript-adobe-experience-cloud/platforms/android/nativescript_adobe_marketing_cloud.aar
index 246d332..cc05730 100644
Binary files a/packages/nativescript-adobe-experience-cloud/platforms/android/nativescript_adobe_marketing_cloud.aar and b/packages/nativescript-adobe-experience-cloud/platforms/android/nativescript_adobe_marketing_cloud.aar differ
diff --git a/packages/nativescript-adobe-experience-cloud/project.json b/packages/nativescript-adobe-experience-cloud/project.json
index 5a10cac..2b149ec 100644
--- a/packages/nativescript-adobe-experience-cloud/project.json
+++ b/packages/nativescript-adobe-experience-cloud/project.json
@@ -5,7 +5,7 @@
"sourceRoot": "packages/nativescript-adobe-experience-cloud",
"targets": {
"build": {
- "executor": "@nrwl/js:tsc",
+ "executor": "@nx/js:tsc",
"options": {
"outputPath": "dist/packages/nativescript-adobe-experience-cloud",
"tsConfig": "packages/nativescript-adobe-experience-cloud/tsconfig.json",
@@ -39,11 +39,10 @@
"dependsOn": [
{
"target": "build.all",
- "projects": "dependencies"
+ "dependencies": true
},
{
- "target": "build",
- "projects": "self"
+ "target": "build"
}
]
},
@@ -55,7 +54,7 @@
}
},
"lint": {
- "executor": "@nrwl/linter:eslint",
+ "executor": "@nx/linter:eslint",
"options": {
"lintFilePatterns": ["packages/nativescript-adobe-experience-cloud/**/*.ts"]
}
diff --git a/packages/nativescript-appdynamics/platforms/android/appdynamics_plugin.aar b/packages/nativescript-appdynamics/platforms/android/appdynamics_plugin.aar
index d80f3a0..3e8f6ce 100644
Binary files a/packages/nativescript-appdynamics/platforms/android/appdynamics_plugin.aar and b/packages/nativescript-appdynamics/platforms/android/appdynamics_plugin.aar differ
diff --git a/packages/nativescript-appdynamics/project.json b/packages/nativescript-appdynamics/project.json
index 22e6de2..f33647b 100644
--- a/packages/nativescript-appdynamics/project.json
+++ b/packages/nativescript-appdynamics/project.json
@@ -5,7 +5,7 @@
"sourceRoot": "packages/nativescript-appdynamics",
"targets": {
"build": {
- "executor": "@nrwl/js:tsc",
+ "executor": "@nx/js:tsc",
"options": {
"outputPath": "dist/packages/nativescript-appdynamics",
"tsConfig": "packages/nativescript-appdynamics/tsconfig.json",
@@ -41,11 +41,10 @@
"dependsOn": [
{
"target": "build.hooks",
- "projects": "dependencies"
+ "dependencies": true
},
{
- "target": "build",
- "projects": "self"
+ "target": "build"
}
]
},
@@ -59,15 +58,13 @@
"dependsOn": [
{
"target": "build.all",
- "projects": "dependencies"
+ "dependencies": true
},
{
- "target": "build.hooks",
- "projects": "self"
+ "target": "build.hooks"
},
{
- "target": "build",
- "projects": "self"
+ "target": "build"
}
]
},
@@ -81,7 +78,7 @@
"dependsOn": [
{
"target": "build.all",
- "projects": "dependencies"
+ "dependencies": true
}
]
},
@@ -93,7 +90,7 @@
}
},
"lint": {
- "executor": "@nrwl/linter:eslint",
+ "executor": "@nx/linter:eslint",
"options": {
"lintFilePatterns": ["packages/nativescript-appdynamics/**/*.ts"]
}
diff --git a/packages/nativescript-iadvize/project.json b/packages/nativescript-iadvize/project.json
index 1302081..6e40be6 100644
--- a/packages/nativescript-iadvize/project.json
+++ b/packages/nativescript-iadvize/project.json
@@ -5,7 +5,7 @@
"sourceRoot": "packages/nativescript-iadvize",
"targets": {
"build": {
- "executor": "@nrwl/js:tsc",
+ "executor": "@nx/js:tsc",
"options": {
"outputPath": "dist/packages/nativescript-iadvize",
"tsConfig": "packages/nativescript-iadvize/tsconfig.json",
@@ -39,11 +39,10 @@
"dependsOn": [
{
"target": "build.all",
- "projects": "dependencies"
+ "dependencies": true
},
{
- "target": "build",
- "projects": "self"
+ "target": "build"
}
]
},
@@ -55,7 +54,7 @@
}
},
"lint": {
- "executor": "@nrwl/linter:eslint",
+ "executor": "@nx/linter:eslint",
"options": {
"lintFilePatterns": ["packages/nativescript-iadvize/**/*.ts"]
}
diff --git a/packages/nativescript-medallia/platforms/android/android-sdk-4.1.0.aar b/packages/nativescript-medallia/platforms/android/android-sdk-4.1.0.aar
index c2baa58..821c386 100644
Binary files a/packages/nativescript-medallia/platforms/android/android-sdk-4.1.0.aar and b/packages/nativescript-medallia/platforms/android/android-sdk-4.1.0.aar differ
diff --git a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/Assets.car b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/Assets.car
index b4fea67..cc94576 100644
Binary files a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/Assets.car and b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/Assets.car differ
diff --git a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/Info.plist b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/Info.plist
index f7686c8..505e3b8 100644
Binary files a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/Info.plist and b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/Info.plist differ
diff --git a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/MedalliaDigitalSDK b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/MedalliaDigitalSDK
index 48c2528..197da41 100755
Binary files a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/MedalliaDigitalSDK and b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/MedalliaDigitalSDK differ
diff --git a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/arm64-apple-ios.swiftdoc b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/arm64-apple-ios.swiftdoc
index e5201e8..b54b4c7 100644
Binary files a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/arm64-apple-ios.swiftdoc and b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/arm64-apple-ios.swiftdoc differ
diff --git a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/armv7-apple-ios.swiftdoc b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/armv7-apple-ios.swiftdoc
index 13b5b5e..bdc138b 100644
Binary files a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/armv7-apple-ios.swiftdoc and b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_armv7/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/armv7-apple-ios.swiftdoc differ
diff --git a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Assets.car b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Assets.car
index b4fea67..cc94576 100644
Binary files a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Assets.car and b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Assets.car differ
diff --git a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Info.plist b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Info.plist
index 417d1b8..f472d6a 100644
Binary files a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Info.plist and b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Info.plist differ
diff --git a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/MedalliaDigitalSDK b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/MedalliaDigitalSDK
index e94f3c1..72291fc 100755
Binary files a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/MedalliaDigitalSDK and b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/MedalliaDigitalSDK differ
diff --git a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc
index 65f7fe1..34619a4 100644
Binary files a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc and b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc differ
diff --git a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/i386-apple-ios-simulator.swiftdoc b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/i386-apple-ios-simulator.swiftdoc
index 4753b4f..9bef6f9 100644
Binary files a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/i386-apple-ios-simulator.swiftdoc and b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/i386-apple-ios-simulator.swiftdoc differ
diff --git a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc
index 57a25e5..f4bfb1d 100644
Binary files a/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc and b/packages/nativescript-medallia/platforms/ios/MedalliaDigitalSDK.xcframework/ios-arm64_i386_x86_64-simulator/MedalliaDigitalSDK.framework/Modules/MedalliaDigitalSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc differ
diff --git a/packages/nativescript-medallia/project.json b/packages/nativescript-medallia/project.json
index 8297cad..701c1db 100644
--- a/packages/nativescript-medallia/project.json
+++ b/packages/nativescript-medallia/project.json
@@ -5,7 +5,7 @@
"sourceRoot": "packages/nativescript-medallia",
"targets": {
"build": {
- "executor": "@nrwl/js:tsc",
+ "executor": "@nx/js:tsc",
"options": {
"outputPath": "dist/packages/nativescript-medallia",
"tsConfig": "packages/nativescript-medallia/tsconfig.json",
@@ -39,11 +39,10 @@
"dependsOn": [
{
"target": "build.all",
- "projects": "dependencies"
+ "dependencies": true
},
{
- "target": "build",
- "projects": "self"
+ "target": "build"
}
]
},
@@ -55,7 +54,7 @@
}
},
"lint": {
- "executor": "@nrwl/linter:eslint",
+ "executor": "@nx/linter:eslint",
"options": {
"lintFilePatterns": ["packages/nativescript-medallia/**/*.ts"]
}
diff --git a/packages/nativescript-ng-sentry/project.json b/packages/nativescript-ng-sentry/project.json
index 2a44227..5ebbb19 100644
--- a/packages/nativescript-ng-sentry/project.json
+++ b/packages/nativescript-ng-sentry/project.json
@@ -5,7 +5,7 @@
"sourceRoot": "packages/nativescript-ng-sentry",
"targets": {
"build": {
- "executor": "@nrwl/js:tsc",
+ "executor": "@nx/js:tsc",
"options": {
"outputPath": "dist/packages/nativescript-ng-sentry",
"tsConfig": "packages/nativescript-ng-sentry/tsconfig.json",
@@ -39,11 +39,10 @@
"dependsOn": [
{
"target": "build.all",
- "projects": "dependencies"
+ "dependencies": true
},
{
- "target": "build",
- "projects": "self"
+ "target": "build"
}
]
},
@@ -55,7 +54,7 @@
}
},
"lint": {
- "executor": "@nrwl/linter:eslint",
+ "executor": "@nx/linter:eslint",
"options": {
"lintFilePatterns": ["packages/nativescript-ng-sentry/**/*.ts"]
}
diff --git a/packages/nativescript-urban-airship/project.json b/packages/nativescript-urban-airship/project.json
index 09cf202..22e2abc 100644
--- a/packages/nativescript-urban-airship/project.json
+++ b/packages/nativescript-urban-airship/project.json
@@ -5,7 +5,7 @@
"sourceRoot": "packages/nativescript-urban-airship",
"targets": {
"build": {
- "executor": "@nrwl/js:tsc",
+ "executor": "@nx/js:tsc",
"options": {
"outputPath": "dist/packages/nativescript-urban-airship",
"tsConfig": "packages/nativescript-urban-airship/tsconfig.json",
@@ -39,11 +39,10 @@
"dependsOn": [
{
"target": "build.all",
- "projects": "dependencies"
+ "dependencies": true
},
{
- "target": "build",
- "projects": "self"
+ "target": "build"
}
]
},
@@ -55,7 +54,7 @@
}
},
"lint": {
- "executor": "@nrwl/linter:eslint",
+ "executor": "@nx/linter:eslint",
"options": {
"lintFilePatterns": ["packages/nativescript-urban-airship/**/*.ts"]
}
diff --git a/packages/nativescript-webview-ext/.eslintrc.json b/packages/nativescript-webview-ext/.eslintrc.json
new file mode 100644
index 0000000..53c06c8
--- /dev/null
+++ b/packages/nativescript-webview-ext/.eslintrc.json
@@ -0,0 +1,18 @@
+{
+ "extends": ["../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*", "node_modules/**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.ts", "*.tsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.js", "*.jsx"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/packages/nativescript-webview-ext/README.md b/packages/nativescript-webview-ext/README.md
new file mode 100644
index 0000000..28f6a14
--- /dev/null
+++ b/packages/nativescript-webview-ext/README.md
@@ -0,0 +1,178 @@
+# @essent/nativescript-webview-ext
+
+Extended WebView for NativeScript which adds "x-local"-custom-scheme for loading local-files, handle events between WebView and NativeScript, JavaScript execution, injecting CSS and JS-files.
+Supports Android 19+ and iOS9+.
+
+**NOTE:** This extends and updates the excellent: https://github.com/Notalib/nativescript-webview-ext
+
+```javascript
+npm install @essent/nativescript-webview-ext
+```
+
+## Features
+* Adds a custom-scheme handler for `x-local://` to the webview for loading of resources inside the webview.
+ * Note: This is **not** supported on iOS <11
+* Adds support for capturing URLs.
+ * This allows the app to open external links in an external browser and handle tel-links
+* Added functions like:
+ - `executeJavaScript(code: string)` for executing JavaScript-code and getting result.
+ - `executePromise(code: string)` for calling promises and getting the result.
+ - `getTitle()` returns document.title.
+* Two-Way event listeners between `NativeScript` and `WebView`
+ * From `NativeScript` to `WebView`
+ * From `WebView` to `NativeScript`
+* Adds functions to inject `css`- and `javascript`-files.
+ * Into the current page.
+ * Auto-injected on page load.
+* Polyfills:
+ * Promise
+ * Fetch API (overrides Native API on Android to support x-local:// and file://)
+* Allows `alert`, `confirm` and `prompt` with `WkWebView`.
+* Supports:
+ * Android 19+
+ * iOS 11+: Full support
+ * iOS <11: Partial support
+
+### Update minSdkVersion to 19 or higher
+
+Android SDK 19 is required, update `App_Resources/Android/app.gradle`:
+```
+android {
+ defaultConfig {
+ minSdkVersion 19 // change this line
+ generatedDensities = []
+ }
+ aaptOptions {
+ additionalParameters "--no-version-vectors"
+ }
+}
+```
+
+### Core support
+Load in template like this:
+
+```xml
+
+
+
+
+
+
+
+```
+
+### Angular support
+
+Import `WebViewExtModule` from `@essent/nativescript-webview-ext/angular` and add it to your `NgModule`.
+
+This registers the element `WebViewExt`. Replace the `` tag with ``
+
+### Vue support
+
+Import `@essent/nativescript-webview-ext/vue` in your app entry file (likely app.js or main.js).
+
+This registers the element `WebViewExt`. Replace the `` tag with ``
+
+## Usage
+
+## Limitations
+
+The custom-scheme handler for `x-local://` is only supported by `Android` and `iOS 11+`
+
+Custom-scheme support for `iOS <11` was removed because of [ITMS-90809](https://forums.developer.apple.com/thread/122114).
+
+## API
+
+### NativeScript View
+
+| Property | Value | Description |
+| --- | --- | --- |
+| readonly supportXLocalScheme | true / false | Is `x-local://` supported? True on `iOS >= 11` or `Android`, False on `iOS < 11`. |
+| src | | Load src |
+| autoInjectJSBridge | true / false | Should the window.nsWebViewBridge be injected on `loadFinishedEvent`? Defaults to true |
+| builtInZoomControls | true / false | Android: Is the built-in zoom mechanisms being used |
+| cacheMode | default / no_cache / cache_first / cache_only | Android: Set caching mode. |
+| databaseStorage | true / false | Android: Enable/Disabled database storage API. Note: It affects all webviews in the process. |
+| debugMode | true / false | Android: Enable chrome debugger for webview on Android. Note: Applies to all webviews in App |
+| displayZoomControls | true / false | Android: displays on-screen zoom controls when using the built-in zoom mechanisms |
+| domStorage | true / false | Android: Enable/Disabled DOM Storage API. E.g localStorage |
+| scrollBounce | true / false | iOS: Should the scrollView bounce? Defaults to true. |
+| supportZoom | true / false | Android: should the webview support zoom |
+| viewPortSize | false / view-port string / ViewPortProperties | Set the viewport metadata on load finished. **Note:** WkWebView sets initial-scale=1.0 by default. |
+| limitsNavigationsToAppBoundDomains | false | iOS: allows to enable Service Workers **Note:** If set to true, WKAppBoundDomains also should be set in info.plist. |
+
+| Function | Description |
+| --- | --- |
+| loadUrl(src: string): Promise | Open a URL and resolves a promise once it has finished loading. |
+| registerLocalResource(resourceName: string, path: string): void; | Map the "x-local://{resourceName}" => "{path}". |
+| unregisterLocalResource(resourceName: string): void; | Removes the mapping from "x-local://{resourceName}" => "{path}" |
+| getRegisteredLocalResource(resourceName: string): void; | Get the mapping from "x-local://{resourceName}" => "{path}" |
+| loadJavaScriptFile(scriptName: string, filepath: string) | Inject a javascript-file into the webview. Should be called after the `loadFinishedEvent` |
+| loadStyleSheetFile(stylesheetName: string, filepath: string, insertBefore: boolean) | Loads a CSS-file into document.head. If before is true, it will be added to the top of document.head otherwise as the last element |
+| loadJavaScriptFiles(files: {resourceName: string, filepath: string}[]) | Inject multiple javascript-files into the webview. Should be called after the `loadFinishedEvent` |
+| loadStyleSheetFiles(files: {resourceName: string, filepath: string, insertBefore: boolean}[]) | Loads multiple CSS-files into the document.head. If before is true, it will be added to the top of document.head otherwise as the last element |
+| autoLoadJavaScriptFile(resourceName: string, filepath: string) | Register a JavaScript-file to be injected on `loadFinishedEvent`. If a page is already loaded, the script will be injected into the current page. |
+| autoLoadStyleSheetFile(resourceName: string, filepath: string, insertBefore?: boolean) | Register a CSS-file to be injected on `loadFinishedEvent`. If a page is already loaded, the CSS-file will be injected into the current page. |
+| autoExecuteJavaScript(scriptCode: string, name: string) | Execute a script on `loadFinishedEvent`. The script can be a promise |
+| executeJavaScript(scriptCode: string) | Execute JavaScript in the webpage. *Note:* scriptCode should be ES5 compatible, or it might not work on 'iOS < 11' |
+| executePromise(scriptCode: string, timeout: number = 500) | Run a promise inside the webview. *Note:* Executing scriptCode must return a promise. |
+| emitToWebView(eventName: string, data: any) | Emit an event to the webview. Note: data must be stringify'able with JSON.stringify or this throws an exception. |
+| getTitle() | Returns a promise with the current document title. |
+
+## Events
+| Event | Description |
+| --- | --- |
+| loadFinished | Raised when a loadFinished event occurs. args is a `LoadFinishedEventData` |
+| loadProgress | Android only: Raised during page load to indicate the progress. args is a `LoadProgressEventData` |
+| loadStarted | Raised when a loadStarted event occurs. args is a `LoadStartedEventData` |
+| shouldOverrideUrlLoading | Raised before the webview requests an URL. Can cancelled by setting args.cancel = true in the `ShouldOverrideUrlLoadEventData` |
+| titleChanged | Document title changed |
+| webAlert | Raised when `window.alert` is triggered inside the webview, needed to use custom dialogs for web alerts. args in a `WebAlertEventData`. `args.callback()` must be called to indicate alert is closed. |
+| webConfirm | Raised when `window.confirm` is triggered inside the webview, needed to use custom dialogs for web confirm boxes. args in a `webConfirmEvent`. `args.callback(boolean)` must be called to indicate confirm box is closed. |
+| webConsole | Android only: Raised when a line is added to the web console. args is a `WebConsoleEventData`. |
+| webPrompt | Raised when `window.prompt` is triggered inside the webview, needed to use custom dialogs for web prompt boxes. args in a `webConfirmEvent`. `args.callback(string | null)` must be called to indicate prompt box is closed. |
+| Events emitted from the webview | Raised when nsWebViewBridge.emit(...) is called inside the webview. args in an `WebViewEventData` |
+
+### WebView
+
+Inside the WebView we have the `nsWebViewBridge` for sending events between the `NativeScript`-layer and the `WebView`.
+**Note:** The bridge will only be available `DOMContentLoaded` or `onload` inside the WebView.
+
+| Function | Description |
+| --- | --- |
+| window.nsWebViewBridge.on(eventName: string, cb: (data: any) => void) | Registers handlers for events from the native layer. |
+| window.nsWebViewBridge.off(eventName: string, cb?: (data: any) => void) | Unregister handlers for events from the native layer. |
+| window.nsWebViewBridge.emit(eventName: string, data: any) | Emits event to NativeScript layer. Will be emitted on the WebViewExt as any other event, data will be a part of the WebViewEventData-object |
+
+#### Waiting for nsWebViewBridge to be available
+
+```javascript
+ window.addEventListener("ns-bridge-ready", function(e) {
+ var nsWebViewBridge = e.detail || window.nsWebViewBridge;
+
+ // do stuff here
+ });
+```
+
+## Possible features to come:
+
+* Cookie helpers?
+* Share cache with native-layer?
+
+### Android
+* Settings
+ * AppCache?
+ * User agent?
+
+#### iOS
+* Settings?
+
+## About Nota
+
+Nota is the Danish Library and Expertise Center for people with print disabilities.
+To become a member of Nota you must be able to document that you cannot read ordinary printed text. Members of Nota are visually impaired, dyslexic or otherwise impaired.
+Our purpose is to ensure equal access to knowledge, community participation and experiences for people who're unable to read ordinary printed text.
+
+## License
+
+Apache License Version 2.0
diff --git a/packages/nativescript-webview-ext/angular/.eslintrc.json b/packages/nativescript-webview-ext/angular/.eslintrc.json
new file mode 100644
index 0000000..e1b9056
--- /dev/null
+++ b/packages/nativescript-webview-ext/angular/.eslintrc.json
@@ -0,0 +1,24 @@
+{
+ "extends": ["../.eslintrc.json"],
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.ts", "*.tsx"],
+ "rules": {
+ "@nx/enforce-module-boundaries": [
+ "error",
+ {
+ "allowCircularSelfDependency": true
+ }
+ ]
+ }
+ },
+ {
+ "files": ["*.js", "*.jsx"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/packages/nativescript-webview-ext/angular/index.ts b/packages/nativescript-webview-ext/angular/index.ts
new file mode 100644
index 0000000..cba2b3d
--- /dev/null
+++ b/packages/nativescript-webview-ext/angular/index.ts
@@ -0,0 +1,8 @@
+import { NgModule } from '@angular/core';
+import { registerElement } from '@nativescript/angular';
+import { WebViewExt } from '@essent/nativescript-webview-ext';
+
+@NgModule()
+export class WebViewExtModule {}
+
+registerElement('WebViewExt', () => WebViewExt);
diff --git a/packages/nativescript-webview-ext/angular/ng-package.json b/packages/nativescript-webview-ext/angular/ng-package.json
new file mode 100644
index 0000000..282622e
--- /dev/null
+++ b/packages/nativescript-webview-ext/angular/ng-package.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
+ "lib": {
+ "entryFile": "index.ts"
+ },
+ "allowedNonPeerDependencies": ["."],
+ "dest": "../../../dist/packages/nativescript-webview-ext/angular"
+}
diff --git a/packages/nativescript-webview-ext/angular/package.json b/packages/nativescript-webview-ext/angular/package.json
new file mode 100644
index 0000000..a3fb897
--- /dev/null
+++ b/packages/nativescript-webview-ext/angular/package.json
@@ -0,0 +1,3 @@
+{
+ "name": "@essent/nativescript-webview-ext-angular"
+}
diff --git a/packages/nativescript-webview-ext/angular/tsconfig.angular.json b/packages/nativescript-webview-ext/angular/tsconfig.angular.json
new file mode 100644
index 0000000..356f64c
--- /dev/null
+++ b/packages/nativescript-webview-ext/angular/tsconfig.angular.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../../../node_modules/ng-packagr/lib/ts/conf/tsconfig.ngc.json",
+ "compilerOptions": {
+ "types": ["node"],
+ "baseUrl": ".",
+ "paths": {
+ "@essent/nativescript-webview-ext": ["../../../dist/packages/nativescript-webview-ext"]
+ },
+ "outDir": "../../../dist/out-tsc",
+ "declarationDir": "../../../dist/out-tsc"
+ },
+ "files": ["index.ts"]
+}
diff --git a/packages/nativescript-webview-ext/angular/tsconfig.json b/packages/nativescript-webview-ext/angular/tsconfig.json
new file mode 100644
index 0000000..0ec69de
--- /dev/null
+++ b/packages/nativescript-webview-ext/angular/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "rootDirs": [".", "../.."]
+ }
+}
diff --git a/packages/nativescript-webview-ext/bridge-loader.ts b/packages/nativescript-webview-ext/bridge-loader.ts
new file mode 100644
index 0000000..3e9a3fd
--- /dev/null
+++ b/packages/nativescript-webview-ext/bridge-loader.ts
@@ -0,0 +1,8 @@
+export const fetchPolyfill: string =
+ '!function(global,factory){"object"==typeof exports&&"undefined"!=typeof module?factory(exports):"function"==typeof define&&define.amd?define(["exports"],factory):factory(global.WHATWGFetch={})}(this,(function(exports){"use strict";var global="undefined"!=typeof globalThis&&globalThis||"undefined"!=typeof self&&self||void 0!==global&&global,support_searchParams="URLSearchParams"in global,support_iterable="Symbol"in global&&"iterator"in Symbol,support_blob="FileReader"in global&&"Blob"in global&&function(){try{return new Blob,!0}catch(e){return!1}}(),support_formData="FormData"in global,support_arrayBuffer="ArrayBuffer"in global;if(support_arrayBuffer)var viewClasses=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],isArrayBufferView=ArrayBuffer.isView||function(obj){return obj&&viewClasses.indexOf(Object.prototype.toString.call(obj))>-1};function normalizeName(name){if("string"!=typeof name&&(name=String(name)),/[^a-z0-9\\-#$%&\'*+.^_`|~!]/i.test(name)||""===name)throw new TypeError(\'Invalid character in header field name: "\'+name+\'"\');return name.toLowerCase()}function normalizeValue(value){return"string"!=typeof value&&(value=String(value)),value}function iteratorFor(items){var iterator={next:function(){var value=items.shift();return{done:void 0===value,value:value}}};return support_iterable&&(iterator[Symbol.iterator]=function(){return iterator}),iterator}function Headers(headers){this.map={},headers instanceof Headers?headers.forEach((function(value,name){this.append(name,value)}),this):Array.isArray(headers)?headers.forEach((function(header){this.append(header[0],header[1])}),this):headers&&Object.getOwnPropertyNames(headers).forEach((function(name){this.append(name,headers[name])}),this)}function consumed(body){if(body.bodyUsed)return Promise.reject(new TypeError("Already read"));body.bodyUsed=!0}function fileReaderReady(reader){return new Promise((function(resolve,reject){reader.onload=function(){resolve(reader.result)},reader.onerror=function(){reject(reader.error)}}))}function readBlobAsArrayBuffer(blob){var reader=new FileReader,promise=fileReaderReady(reader);return reader.readAsArrayBuffer(blob),promise}function bufferClone(buf){if(buf.slice)return buf.slice(0);var view=new Uint8Array(buf.byteLength);return view.set(new Uint8Array(buf)),view.buffer}function Body(){return this.bodyUsed=!1,this._initBody=function(body){var obj;this.bodyUsed=this.bodyUsed,this._bodyInit=body,body?"string"==typeof body?this._bodyText=body:support_blob&&Blob.prototype.isPrototypeOf(body)?this._bodyBlob=body:support_formData&&FormData.prototype.isPrototypeOf(body)?this._bodyFormData=body:support_searchParams&&URLSearchParams.prototype.isPrototypeOf(body)?this._bodyText=body.toString():support_arrayBuffer&&support_blob&&((obj=body)&&DataView.prototype.isPrototypeOf(obj))?(this._bodyArrayBuffer=bufferClone(body.buffer),this._bodyInit=new Blob([this._bodyArrayBuffer])):support_arrayBuffer&&(ArrayBuffer.prototype.isPrototypeOf(body)||isArrayBufferView(body))?this._bodyArrayBuffer=bufferClone(body):this._bodyText=body=Object.prototype.toString.call(body):this._bodyText="",this.headers.get("content-type")||("string"==typeof body?this.headers.set("content-type","text/plain;charset=UTF-8"):this._bodyBlob&&this._bodyBlob.type?this.headers.set("content-type",this._bodyBlob.type):support_searchParams&&URLSearchParams.prototype.isPrototypeOf(body)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))},support_blob&&(this.blob=function(){var rejected=consumed(this);if(rejected)return rejected;if(this._bodyBlob)return Promise.resolve(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(new Blob([this._bodyArrayBuffer]));if(this._bodyFormData)throw new Error("could not read FormData body as blob");return Promise.resolve(new Blob([this._bodyText]))},this.arrayBuffer=function(){if(this._bodyArrayBuffer){var isConsumed=consumed(this);return isConsumed||(ArrayBuffer.isView(this._bodyArrayBuffer)?Promise.resolve(this._bodyArrayBuffer.buffer.slice(this._bodyArrayBuffer.byteOffset,this._bodyArrayBuffer.byteOffset+this._bodyArrayBuffer.byteLength)):Promise.resolve(this._bodyArrayBuffer))}return this.blob().then(readBlobAsArrayBuffer)}),this.text=function(){var blob,reader,promise,rejected=consumed(this);if(rejected)return rejected;if(this._bodyBlob)return blob=this._bodyBlob,reader=new FileReader,promise=fileReaderReady(reader),reader.readAsText(blob),promise;if(this._bodyArrayBuffer)return Promise.resolve(function(buf){for(var view=new Uint8Array(buf),chars=new Array(view.length),i=0;i-1?upcased:method),this.mode=options.mode||this.mode||null,this.signal=options.signal||this.signal,this.referrer=null,("GET"===this.method||"HEAD"===this.method)&&body)throw new TypeError("Body not allowed for GET or HEAD requests");if(this._initBody(body),!("GET"!==this.method&&"HEAD"!==this.method||"no-store"!==options.cache&&"no-cache"!==options.cache)){var reParamSearch=/([?&])_=[^&]*/;if(reParamSearch.test(this.url))this.url=this.url.replace(reParamSearch,"$1_="+(new Date).getTime());else{this.url+=(/\\?/.test(this.url)?"&":"?")+"_="+(new Date).getTime()}}}function decode(body){var form=new FormData;return body.trim().split("&").forEach((function(bytes){if(bytes){var split=bytes.split("="),name=split.shift().replace(/\\+/g," "),value=split.join("=").replace(/\\+/g," ");form.append(decodeURIComponent(name),decodeURIComponent(value))}})),form}function Response(bodyInit,options){if(!(this instanceof Response))throw new TypeError(\'Please use the "new" operator, this DOM object constructor cannot be called as a function.\');options||(options={}),this.type="default",this.status=void 0===options.status?200:options.status,this.ok=this.status>=200&&this.status<300,this.statusText=void 0===options.statusText?"":""+options.statusText,this.headers=new Headers(options.headers),this.url=options.url||"",this._initBody(bodyInit)}Request.prototype.clone=function(){return new Request(this,{body:this._bodyInit})},Body.call(Request.prototype),Body.call(Response.prototype),Response.prototype.clone=function(){return new Response(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new Headers(this.headers),url:this.url})},Response.error=function(){var response=new Response(null,{status:0,statusText:""});return response.type="error",response};var redirectStatuses=[301,302,303,307,308];Response.redirect=function(url,status){if(-1===redirectStatuses.indexOf(status))throw new RangeError("Invalid status code");return new Response(null,{status:status,headers:{location:url}})},exports.DOMException=global.DOMException;try{new exports.DOMException}catch(err){exports.DOMException=function(message,name){this.message=message,this.name=name;var error=Error(message);this.stack=error.stack},exports.DOMException.prototype=Object.create(Error.prototype),exports.DOMException.prototype.constructor=exports.DOMException}function fetch(input,init){return new Promise((function(resolve,reject){var request=new Request(input,init);if(request.signal&&request.signal.aborted)return reject(new exports.DOMException("Aborted","AbortError"));var xhr=new XMLHttpRequest;function abortXhr(){xhr.abort()}xhr.onload=function(){var rawHeaders,headers,options={status:xhr.status,statusText:xhr.statusText,headers:(rawHeaders=xhr.getAllResponseHeaders()||"",headers=new Headers,rawHeaders.replace(/\\r?\\n[\\t ]+/g," ").split("\\r").map((function(header){return 0===header.indexOf("\\n")?header.substr(1,header.length):header})).forEach((function(line){var parts=line.split(":"),key=parts.shift().trim();if(key){var value=parts.join(":").trim();headers.append(key,value)}})),headers)};options.url="responseURL"in xhr?xhr.responseURL:options.headers.get("X-Request-URL");var body="response"in xhr?xhr.response:xhr.responseText;setTimeout((function(){resolve(new Response(body,options))}),0)},xhr.onerror=function(){setTimeout((function(){reject(new TypeError("Network request failed"))}),0)},xhr.ontimeout=function(){setTimeout((function(){reject(new TypeError("Network request failed"))}),0)},xhr.onabort=function(){setTimeout((function(){reject(new exports.DOMException("Aborted","AbortError"))}),0)},xhr.open(request.method,function(url){try{return""===url&&global.location.href?global.location.href:url}catch(e){return url}}(request.url),!0),"include"===request.credentials?xhr.withCredentials=!0:"omit"===request.credentials&&(xhr.withCredentials=!1),"responseType"in xhr&&(support_blob?xhr.responseType="blob":support_arrayBuffer&&request.headers.get("Content-Type")&&-1!==request.headers.get("Content-Type").indexOf("application/octet-stream")&&(xhr.responseType="arraybuffer")),!init||"object"!=typeof init.headers||init.headers instanceof Headers?request.headers.forEach((function(value,name){xhr.setRequestHeader(name,value)})):Object.getOwnPropertyNames(init.headers).forEach((function(name){xhr.setRequestHeader(name,normalizeValue(init.headers[name]))})),request.signal&&(request.signal.addEventListener("abort",abortXhr),xhr.onreadystatechange=function(){4===xhr.readyState&&request.signal.removeEventListener("abort",abortXhr)}),xhr.send(void 0===request._bodyInit?null:request._bodyInit)}))}fetch.polyfill=!0,global.fetch||(global.fetch=fetch,global.Headers=Headers,global.Request=Request,global.Response=Response),exports.Headers=Headers,exports.Request=Request,exports.Response=Response,exports.fetch=fetch,Object.defineProperty(exports,"__esModule",{value:!0})}));';
+export const promisePolyfill: string =
+ '!function(global,factory){"object"==typeof exports&&"undefined"!=typeof module?factory():"function"==typeof define&&define.amd?define(factory):factory()}(0,(function(){"use strict";function finallyConstructor(callback){var constructor=this.constructor;return this.then((function(value){return constructor.resolve(callback()).then((function(){return value}))}),(function(reason){return constructor.resolve(callback()).then((function(){return constructor.reject(reason)}))}))}function allSettled(arr){return new this((function(resolve,reject){if(!arr||void 0===arr.length)return reject(new TypeError(typeof arr+" "+arr+" is not iterable(cannot read property Symbol(Symbol.iterator))"));var args=Array.prototype.slice.call(arr);if(0===args.length)return resolve([]);var remaining=args.length;function res(i,val){if(val&&("object"==typeof val||"function"==typeof val)){var then=val.then;if("function"==typeof then)return void then.call(val,(function(val){res(i,val)}),(function(e){args[i]={status:"rejected",reason:e},0==--remaining&&resolve(args)}))}args[i]={status:"fulfilled",value:val},0==--remaining&&resolve(args)}for(var i=0;i0&&t[t.length-1])||6!==op[0]&&2!==op[0])){_=0;continue}if(3===op[0]&&(!t||op[1]>t[0]&&op[1]=o.length&&(o=void 0),{value:o&&o[i++],done:!o}}};throw new TypeError(s?"Object is not iterable.":"Symbol.iterator is not defined.")},__read=this&&this.__read||function(o,n){var m="function"==typeof Symbol&&o[Symbol.iterator];if(!m)return o;var r,e,i=m.call(o),ar=[];try{for(;(void 0===n||n-- >0)&&!(r=i.next()).done;)ar.push(r.value)}catch(error){e={error:error}}finally{try{r&&!r.done&&(m=i.return)&&m.call(i)}finally{if(e)throw e.error}}return ar};function getWkWebViewMessageHandler(){var _a,_b,w=window;if(null===(_b=null===(_a=null==w?void 0:w.webkit)||void 0===_a?void 0:_a.messageHandlers)||void 0===_b?void 0:_b.nsBridge)return w.webkit.messageHandlers.nsBridge;console.error("Cannot get the window.webkit.messageHandlers.nsBridge - we can\'t communicate with native-layer")}Object.keys||(Object.keys=function(){"use strict";var hasOwnProperty=Object.prototype.hasOwnProperty,hasDontEnumBug=!{toString:null}.propertyIsEnumerable("toString"),dontEnums=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],dontEnumsLength=dontEnums.length;return function(obj){if("function"!=typeof obj&&("object"!=typeof obj||null===obj))throw new TypeError("Object.keys called on non-object");var result=new Array;for(var prop in obj)hasOwnProperty.call(obj,prop)&&result.push(prop);if(hasDontEnumBug)for(var i=0;i0?document.head.insertBefore(linkElement,document.head.firstElementChild):document.head.appendChild(linkElement))}))},NSWebViewBridge.prototype.injectStyleSheet=function(elId,stylesheet,insertBefore){return document.getElementById(elId)?(console.log("".concat(elId," already exists")),Promise.resolve()):new Promise((function(resolve,reject){var _a,styleElement=document.createElement("style");styleElement.addEventListener("error",reject),styleElement.textContent=stylesheet,styleElement.setAttribute("id",elId);var parentElement=null!==(_a=document.head)&&void 0!==_a?_a:document.body;parentElement?(insertBefore&&parentElement.childElementCount>0?document.head.insertBefore(styleElement,parentElement.firstElementChild):document.head.appendChild(styleElement),resolve()):reject(new Error("Couldn\'t find parent element"))}))},NSWebViewBridge.prototype.executePromise=function(promise,eventName){return __awaiter(this,void 0,void 0,(function(){var data,err_1;return __generator(this,(function(_a){switch(_a.label){case 0:return _a.trys.push([0,2,,3]),[4,promise];case 1:return data=_a.sent(),this.emit(eventName,{data:data}),[3,3];case 2:return err_1=_a.sent(),this.emitError(err_1,eventName),[3,3];case 3:return[2]}}))}))},NSWebViewBridge.prototype.emitError=function(err,eventName){void 0===eventName&&(eventName="web-error"),"object"==typeof err&&(null==err?void 0:err.message)?this.emit(eventName,{err:this.serializeError(err)}):this.emit(eventName,{err:err})},NSWebViewBridge.prototype.elementIdFromHref=function(href){return href.replace(/^[:]*:\\/\\//,"").replace(/[^a-z0-9]/g,"")},NSWebViewBridge.prototype.serializeError=function(error){var e_2,_a,res={name:error.name,message:error.message,stack:error.stack};try{for(var _b=__values(Object.entries(error)),_c=_b.next();!_c.done;_c=_b.next()){var _d=__read(_c.value,2),key=_d[0],value=_d[1];value instanceof HTMLElement||(key in res||(res[key]=value))}}catch(e_2_1){e_2={error:e_2_1}}finally{try{_c&&!_c.done&&(_a=_b.return)&&_a.call(_b)}finally{if(e_2)throw e_2.error}}return res},NSWebViewBridge}(),nsBridgeReadyEventName="ns-bridge-ready";"nsWebViewBridge"in window||function(fn){window.nsWebViewBridge=new NSWebViewBridge,"complete"===document.readyState||"interactive"===document.readyState?setTimeout(fn,1):document.addEventListener("DOMContentLoaded",fn)}((function(){var e_3,_a;try{for(var _b=__values([nsBridgeReadyEventName,"ns-brige-ready"]),_c=_b.next();!_c.done;_c=_b.next()){var eventName=_c.value;"undefined"!=typeof CustomEvent?window.dispatchEvent(new CustomEvent(eventName,{detail:window.nsWebViewBridge})):window.dispatchEvent(new Event(eventName))}}catch(e_3_1){e_3={error:e_3_1}}finally{try{_c&&!_c.done&&(_a=_b.return)&&_a.call(_b)}finally{if(e_3)throw e_3.error}}}));';
+export const metadataViewPort: string =
+ '!function(window){var defaultViewPort={initialScale:1},document=window.document,meta=document.querySelector(\'head meta[name="viewport"]\');meta||((meta=document.createElement("meta")).setAttribute("name","viewport"),document.head.appendChild(meta));var viewPortValues=defaultViewPort;var _a=viewPortValues.initialScale,initialScale=void 0===_a?defaultViewPort.initialScale:_a,width=viewPortValues.width,height=viewPortValues.height,userScalable=viewPortValues.userScalable,minimumScale=viewPortValues.minimumScale,maximumScale=viewPortValues.maximumScale,content=["initial-scale=".concat(initialScale)];if(width&&content.push("width=".concat(width)),height&&content.push("height=".concat(height)),"boolean"==typeof userScalable)content.push("user-scalable=".concat(userScalable?"yes":"no"));else if("string"==typeof userScalable){var lcUserScalable="".concat(userScalable).toLowerCase();"yes"===lcUserScalable?content.push("user-scalable=yes"):"no"===lcUserScalable?content.push("user-scalable=no"):console.error("userScalable=".concat(JSON.stringify(userScalable)," is an unknown value"))}minimumScale&&content.push("minimum-scale=".concat(minimumScale)),maximumScale&&content.push("maximum-scale=".concat(maximumScale)),meta.setAttribute("content",content.join(", "))}(window);';
diff --git a/packages/nativescript-webview-ext/common.ts b/packages/nativescript-webview-ext/common.ts
new file mode 100644
index 0000000..d331c1e
--- /dev/null
+++ b/packages/nativescript-webview-ext/common.ts
@@ -0,0 +1,1496 @@
+import '@nativescript/core';
+import { booleanConverter, ContainerView, CSSType, EventData, File, knownFolders, path, Property, Trace } from '@nativescript/core';
+import { isEnabledProperty } from '@nativescript/core/ui/core/view';
+import { fetchPolyfill, metadataViewPort, promisePolyfill, webViewBridge } from './bridge-loader';
+
+export interface ViewPortProperties {
+ width?: number | 'device-width';
+ height?: number | 'device-height';
+ initialScale?: number;
+ maximumScale?: number;
+ minimumScale?: number;
+ userScalable?: boolean;
+}
+
+export type CacheMode = 'default' | 'cache_first' | 'no_cache' | 'cache_only' | 'normal';
+
+export const autoInjectJSBridgeProperty = new Property({
+ name: 'autoInjectJSBridge',
+ defaultValue: true,
+ valueConverter: booleanConverter,
+});
+
+export const builtInZoomControlsProperty = new Property({
+ name: 'builtInZoomControls',
+ defaultValue: true,
+ valueConverter: booleanConverter,
+});
+
+export const cacheModeProperty = new Property({
+ name: 'cacheMode',
+ defaultValue: 'default',
+});
+
+export const databaseStorageProperty = new Property({
+ name: 'databaseStorage',
+ defaultValue: false,
+ valueConverter: booleanConverter,
+});
+
+export const domStorageProperty = new Property({
+ name: 'domStorage',
+ defaultValue: false,
+ valueConverter: booleanConverter,
+});
+
+export const debugModeProperty = new Property({
+ name: 'debugMode',
+ defaultValue: false,
+ valueConverter: booleanConverter,
+});
+
+export const displayZoomControlsProperty = new Property({
+ name: 'displayZoomControls',
+ defaultValue: true,
+ valueConverter: booleanConverter,
+});
+
+export const supportZoomProperty = new Property({
+ name: 'supportZoom',
+ defaultValue: false,
+ valueConverter: booleanConverter,
+});
+
+export const srcProperty = new Property({
+ name: 'src',
+});
+
+export const scrollBounceProperty = new Property({
+ name: 'scrollBounce',
+ valueConverter: booleanConverter,
+});
+
+export const limitsNavigationsToAppBoundDomainsProperty = new Property({
+ name: 'limitsNavigationsToAppBoundDomains',
+ valueConverter: booleanConverter,
+});
+
+export type ViewPortValue = boolean | ViewPortProperties;
+export const viewPortProperty = new Property({
+ name: 'viewPortSize',
+ defaultValue: false,
+ valueConverter(value: string | ViewPortProperties): ViewPortValue {
+ const defaultViewPort: ViewPortProperties = {
+ initialScale: 1.0,
+ };
+
+ const valueLowerCaseStr = `${value || ''}`.toLowerCase();
+ if (valueLowerCaseStr === 'false') {
+ return false;
+ } else if (valueLowerCaseStr === 'true' || valueLowerCaseStr === '') {
+ return defaultViewPort;
+ }
+
+ let viewPortInputValues = { ...defaultViewPort };
+
+ if (typeof value === 'object') {
+ viewPortInputValues = { ...value };
+ } else if (typeof value === 'string') {
+ try {
+ viewPortInputValues = JSON.parse(value) as ViewPortProperties;
+ } catch (err) {
+ for (const part of value.split(',').map((v) => v.trim())) {
+ if (!part) {
+ continue;
+ }
+
+ const [key, v] = part.split('=').map((v) => v.trim());
+ if (!key || !v) {
+ continue;
+ }
+
+ const lcValue = `${v}`.toLowerCase();
+ switch (key) {
+ case 'user-scalable':
+ case 'userScalable': {
+ switch (lcValue) {
+ case 'yes':
+ case 'true': {
+ viewPortInputValues.userScalable = true;
+ break;
+ }
+ case 'no':
+ case 'false': {
+ viewPortInputValues.userScalable = false;
+ break;
+ }
+ }
+ break;
+ }
+
+ case 'width': {
+ if (lcValue === 'device-width') {
+ viewPortInputValues.width = 'device-width';
+ } else {
+ viewPortInputValues.width = Number(v);
+ }
+ break;
+ }
+
+ case 'height': {
+ if (lcValue === 'device-height') {
+ viewPortInputValues.height = 'device-height';
+ } else {
+ viewPortInputValues.height = Number(v);
+ }
+ break;
+ }
+
+ case 'minimumScale':
+ case 'minimum-scale': {
+ viewPortInputValues.minimumScale = Number(v);
+ break;
+ }
+ case 'maximumScale':
+ case 'maximum-scale': {
+ viewPortInputValues.maximumScale = Number(v);
+ break;
+ }
+ case 'initialScale':
+ case 'initial-scale': {
+ viewPortInputValues.initialScale = Number(v);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ const { initialScale = defaultViewPort.initialScale, width, height, userScalable, minimumScale, maximumScale } = viewPortInputValues;
+
+ return {
+ initialScale,
+ width,
+ height,
+ userScalable,
+ minimumScale,
+ maximumScale,
+ };
+ },
+});
+
+export enum EventNames {
+ LoadFinished = 'loadFinished',
+ LoadProgress = 'loadProgress',
+ LoadStarted = 'loadStarted',
+ ShouldOverrideUrlLoading = 'shouldOverrideUrlLoading',
+ TitleChanged = 'titleChanged',
+ WebAlert = 'webAlert',
+ WebConfirm = 'webConfirm',
+ WebConsole = 'webConsole',
+ EnterFullscreen = 'enterFullscreen',
+ ExitFullscreen = 'exitFullscreen',
+ WebPrompt = 'webPrompt',
+}
+
+export interface LoadJavaScriptResource {
+ resourceName: string;
+ filepath: string;
+}
+
+export interface LoadStyleSheetResource {
+ resourceName: string;
+ filepath: string;
+ insertBefore?: boolean;
+}
+
+export interface InjectExecuteJavaScript {
+ scriptCode: string;
+ name: string;
+}
+
+export interface WebViewExtEventData extends EventData {
+ object: WebViewExtBase;
+}
+
+/**
+ * Event data containing information for the loading events of a WebView.
+ */
+export interface LoadEventData extends WebViewExtEventData {
+ /**
+ * Gets the url of the web-view.
+ */
+ url: string;
+
+ /**
+ * Gets the navigation type of the web-view.
+ */
+ navigationType?: NavigationType;
+
+ /**
+ * Gets the error (if any).
+ */
+ error?: string;
+}
+
+export interface LoadStartedEventData extends LoadEventData {
+ eventName: EventNames.LoadStarted;
+}
+
+export interface LoadFinishedEventData extends LoadEventData {
+ eventName: EventNames.LoadFinished;
+}
+
+export interface ShouldOverrideUrlLoadEventData extends LoadEventData {
+ eventName: EventNames.ShouldOverrideUrlLoading;
+
+ httpMethod: string;
+
+ /** Flip this to true in your callback, if you want to cancel the url-loading */
+ cancel?: boolean;
+}
+
+/** BackForward compat for spelling error... */
+export interface ShouldOverideUrlLoadEventData extends ShouldOverrideUrlLoadEventData {}
+
+export interface LoadProgressEventData extends WebViewExtEventData {
+ eventName: EventNames.LoadProgress;
+ url: string;
+ progress: number;
+}
+
+export interface TitleChangedEventData extends WebViewExtEventData {
+ eventName: EventNames.TitleChanged;
+ url: string;
+ title: string;
+}
+
+export interface WebAlertEventData extends WebViewExtEventData {
+ eventName: EventNames.WebAlert;
+ url: string;
+ message: string;
+ callback: () => void;
+}
+
+export interface WebPromptEventData extends WebViewExtEventData {
+ eventName: EventNames.WebPrompt;
+ url: string;
+ message: string;
+ defaultText?: string;
+ callback: (response?: string) => void;
+}
+
+export interface WebConfirmEventData extends WebViewExtEventData {
+ eventName: EventNames.WebConfirm;
+ url: string;
+ message: string;
+ callback: (response: boolean) => void;
+}
+
+export interface WebConsoleEventData extends WebViewExtEventData {
+ eventName: EventNames.WebConsole;
+ url: string;
+ data: {
+ lineNo: number;
+ message: string;
+ level: string;
+ };
+}
+
+/**
+ * Event data containing information for the loading events of a WebView.
+ */
+export interface WebViewEventData extends WebViewExtEventData {
+ data?: any;
+}
+
+export interface EnterFullscreenEventData extends WebViewExtEventData {
+ eventName: EventNames.EnterFullscreen;
+ url: string;
+ exitFullscreen(): void;
+}
+
+export interface ExitFullscreenEventData extends WebViewExtEventData {
+ eventName: EventNames.ExitFullscreen;
+ url: string;
+}
+
+/**
+ * Represents navigation type
+ */
+export type NavigationType = 'linkClicked' | 'formSubmitted' | 'backForward' | 'reload' | 'formResubmitted' | 'other' | void;
+
+export class UnsupportedSDKError extends Error {
+ constructor(minSdk: number) {
+ super(`Android API < ${minSdk} not supported`);
+
+ Object.setPrototypeOf(this, UnsupportedSDKError.prototype);
+ }
+}
+
+@CSSType('WebView')
+export class WebViewExtBase extends ContainerView {
+ public static readonly supportXLocalScheme: boolean;
+
+ /**
+ * Is Fetch API supported?
+ *
+ * Note: Android's Native Fetch API needs to be replaced with the polyfill.
+ */
+ public static isFetchSupported: boolean;
+
+ /**
+ * Does this platform's WebView support promises?
+ */
+ public static isPromiseSupported: boolean;
+
+ /**
+ * Gets the native [android widget](http://developer.android.com/reference/android/webkit/WebView.html) that represents the user interface for this component. Valid only when running on Android OS.
+ */
+ public android: any /* android.webkit.WebView */;
+
+ /**
+ * Gets the native [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview/) that represents the user interface for this component. Valid only when running on iOS 11+.
+ */
+ public ios: any /* WKWebView */;
+
+ public get interceptScheme() {
+ return 'x-local';
+ }
+
+ /**
+ * String value used when hooking to loadStarted event.
+ */
+ public static get loadStartedEvent() {
+ return EventNames.LoadStarted;
+ }
+
+ /**
+ * String value used when hooking to loadFinished event.
+ */
+ public static get loadFinishedEvent() {
+ return EventNames.LoadFinished;
+ }
+
+ /** String value used when hooking to shouldOverrideUrlLoading event */
+ public static get shouldOverrideUrlLoadingEvent() {
+ return EventNames.ShouldOverrideUrlLoading;
+ }
+
+ public static get loadProgressEvent() {
+ return EventNames.LoadProgress;
+ }
+
+ public static get titleChangedEvent() {
+ return EventNames.TitleChanged;
+ }
+ public static get webAlertEvent() {
+ return EventNames.WebAlert;
+ }
+ public static get webConfirmEvent() {
+ return EventNames.WebConfirm;
+ }
+ public static get webPromptEvent() {
+ return EventNames.WebPrompt;
+ }
+ public static get webConsoleEvent() {
+ return EventNames.WebConsole;
+ }
+ public static get enterFullscreenEvent() {
+ return EventNames.EnterFullscreen;
+ }
+ public static get exitFullscreenEvent() {
+ return EventNames.ExitFullscreen;
+ }
+
+ public readonly supportXLocalScheme: boolean;
+
+ /**
+ * Gets or sets the url, local file path or HTML string.
+ */
+ public src: string;
+
+ /**
+ * Auto Inject WebView JavaScript Bridge on load finished? Defaults to true.
+ */
+ public autoInjectJSBridge = true;
+
+ /**
+ * Android: Enable/disable debug-mode
+ */
+ public debugMode: boolean;
+
+ /**
+ * Android: Is the built-in zoom mechanisms being used
+ */
+ public builtInZoomControls: boolean;
+
+ /**
+ * Android: displays on-screen zoom controls when using the built-in zoom mechanisms
+ */
+ public displayZoomControls: boolean;
+
+ /**
+ * Android: Enable/Disabled database storage API.
+ * Note: It affects all webviews in the process.
+ */
+ public databaseStorage: boolean;
+
+ /**
+ * Android: Enable/Disabled DOM Storage API. E.g localStorage
+ */
+ public domStorage: boolean;
+
+ /**
+ * Android: should the webview support zoom
+ */
+ public supportZoom: boolean;
+
+ /**
+ * iOS: Should the scrollView bounce? Defaults to true.
+ */
+ public scrollBounce: boolean;
+
+ /**
+ * Set viewport metadata for the webview.
+ * Set to false to disable.
+ *
+ * **Note**: WkWebView defaults initial-scale=1.0.
+ */
+ public viewPortSize: ViewPortValue;
+
+ public cacheMode: 'default' | 'no_cache' | 'cache_first' | 'cache_only';
+
+ /**
+ * List of js-files to be auto injected on load finished
+ */
+ protected autoInjectScriptFiles = [] as LoadJavaScriptResource[];
+
+ /**
+ * List of css-files to be auto injected on load finished
+ */
+ protected autoInjectStyleSheetFiles = [] as LoadStyleSheetResource[];
+
+ /**
+ * List of code blocks to be executed after JS-files and CSS-files have been loaded.
+ */
+ protected autoInjectJavaScriptBlocks = [] as InjectExecuteJavaScript[];
+
+ /**
+ * Prevent this.src loading changes from the webview's onLoadFinished-event
+ */
+ protected tempSuspendSrcLoading = false;
+
+ /**
+ * Callback for the loadFinished-event. Called from the native-webview
+ */
+ public async _onLoadFinished(url: string, error?: string): Promise {
+ url = this.normalizeURL(url);
+
+ if (!error) {
+ // When this is called without an error, update with this.src value without loading the url.
+ // This is needed to keep src up-to-date when linked are clicked inside the webview.
+ try {
+ this.tempSuspendSrcLoading = true;
+ this.src = url;
+ this.tempSuspendSrcLoading = false;
+ } finally {
+ this.tempSuspendSrcLoading = false;
+ }
+ }
+
+ let args = {
+ error,
+ eventName: WebViewExtBase.loadFinishedEvent,
+ navigationType: undefined,
+ object: this,
+ url,
+ } as LoadFinishedEventData;
+
+ if (error) {
+ this.notify(args);
+
+ throw args;
+ }
+
+ this.writeTrace(`WebViewExt._onLoadFinished("${url}", ${error || void 0}) - > Injecting webview-bridge JS code`);
+
+ if (!this.autoInjectJSBridge) {
+ return args;
+ }
+
+ try {
+ await this.injectWebViewBridge();
+
+ await this.loadJavaScriptFiles(this.autoInjectScriptFiles);
+ await this.loadStyleSheetFiles(this.autoInjectStyleSheetFiles);
+ await this.executePromises(
+ this.autoInjectJavaScriptBlocks.map((data) => data.scriptCode),
+ -1
+ );
+ } catch (error) {
+ args.error = error;
+ }
+
+ this.notify(args);
+
+ this.getTitle()
+ .then((title) => title && this._titleChanged(title))
+ .catch(() => void 0);
+
+ return args;
+ }
+
+ /**
+ * Callback for onLoadStarted-event from the native webview
+ *
+ * @param url URL being loaded
+ * @param navigationType Type of navigation (iOS-only)
+ */
+ public _onLoadStarted(url: string, navigationType?: NavigationType) {
+ const args = {
+ eventName: WebViewExtBase.loadStartedEvent,
+ navigationType,
+ object: this,
+ url,
+ } as LoadStartedEventData;
+
+ this.notify(args);
+ }
+
+ /**
+ * Callback for should override url loading.
+ * Called from the native-webview
+ *
+ * @param url
+ * @param httpMethod GET, POST etc
+ * @param navigationType Type of navigation (iOS-only)
+ */
+ public _onShouldOverrideUrlLoading(url: string, httpMethod: string, navigationType?: NavigationType) {
+ const args = {
+ eventName: WebViewExtBase.shouldOverrideUrlLoadingEvent,
+ httpMethod,
+ navigationType,
+ object: this,
+ url,
+ } as ShouldOverrideUrlLoadEventData;
+ this.notify(args);
+
+ const eventNameWithSpellingError = 'shouldOverideUrlLoading';
+ if (this.hasListeners(eventNameWithSpellingError)) {
+ console.error(`eventName '${eventNameWithSpellingError}' is deprecated due to spelling error:\nPlease use: ${WebViewExtBase.shouldOverrideUrlLoadingEvent}`);
+ const argsWithSpellingError = {
+ ...args,
+ eventName: eventNameWithSpellingError,
+ };
+
+ this.notify(argsWithSpellingError);
+ if (argsWithSpellingError.cancel) {
+ return argsWithSpellingError.cancel;
+ }
+ }
+
+ return args.cancel;
+ }
+
+ public _loadProgress(progress: number) {
+ const args = {
+ eventName: WebViewExtBase.loadProgressEvent,
+ object: this,
+ progress,
+ url: this.src,
+ } as LoadProgressEventData;
+
+ this.notify(args);
+ }
+
+ public _titleChanged(title: string) {
+ const args = {
+ eventName: WebViewExtBase.titleChangedEvent,
+ object: this,
+ title,
+ url: this.src,
+ } as TitleChangedEventData;
+
+ this.notify(args);
+ }
+
+ public _webAlert(message: string, callback: () => void) {
+ if (!this.hasListeners(WebViewExtBase.webAlertEvent)) {
+ return false;
+ }
+
+ const args = {
+ eventName: WebViewExtBase.webAlertEvent,
+ object: this,
+ message,
+ url: this.src,
+ callback,
+ } as WebAlertEventData;
+
+ this.notify(args);
+
+ return true;
+ }
+
+ public _webConfirm(message: string, callback: (response: boolean | null) => void) {
+ if (!this.hasListeners(WebViewExtBase.webConfirmEvent)) {
+ return false;
+ }
+
+ const args = {
+ eventName: WebViewExtBase.webConfirmEvent,
+ object: this,
+ message,
+ url: this.src,
+ callback,
+ } as WebConfirmEventData;
+
+ this.notify(args);
+
+ return true;
+ }
+
+ public _webPrompt(message: string, defaultText: string, callback: (response: string | null) => void) {
+ if (!this.hasListeners(WebViewExtBase.webPromptEvent)) {
+ return false;
+ }
+
+ const args = {
+ eventName: WebViewExtBase.webPromptEvent,
+ object: this,
+ message,
+ defaultText,
+ url: this.src,
+ callback,
+ } as WebPromptEventData;
+
+ this.notify(args);
+
+ return true;
+ }
+
+ public _webConsole(message: string, lineNo: number, level: string) {
+ if (!this.hasListeners(WebViewExtBase.webConsoleEvent)) {
+ return false;
+ }
+
+ const args = {
+ eventName: WebViewExtBase.webConsoleEvent,
+ object: this,
+ data: {
+ message,
+ lineNo,
+ level,
+ },
+ url: this.src,
+ } as WebConsoleEventData;
+
+ this.notify(args);
+
+ return true;
+ }
+
+ public _onEnterFullscreen(exitFullscreen: () => void) {
+ if (!this.hasListeners(WebViewExtBase.enterFullscreenEvent)) {
+ return false;
+ }
+
+ const args = {
+ eventName: WebViewExtBase.enterFullscreenEvent,
+ object: this,
+ exitFullscreen,
+ url: this.src,
+ } as EnterFullscreenEventData;
+
+ this.notify(args);
+
+ return true;
+ }
+
+ public _onExitFullscreen() {
+ const args = {
+ eventName: WebViewExtBase.exitFullscreenEvent,
+ object: this,
+ url: this.src,
+ } as ExitFullscreenEventData;
+
+ this.notify(args);
+
+ return true;
+ }
+
+ /**
+ * Platform specific loadURL-implementation.
+ */
+ public _loadUrl(src: string): void {
+ throw new Error('Method not implemented.');
+ }
+
+ /**
+ * Platform specific loadData-implementation.
+ */
+ public _loadData(src: string): void {
+ throw new Error('Method not implemented.');
+ }
+
+ /**
+ * Stops loading the current content (if any).
+ */
+ public stopLoading() {
+ throw new Error('Method not implemented.');
+ }
+
+ /**
+ * Gets a value indicating whether the WebView can navigate back.
+ */
+ public get canGoBack(): boolean {
+ throw new Error('This member is abstract.');
+ }
+
+ /**
+ * Gets a value indicating whether the WebView can navigate forward.
+ */
+ public get canGoForward(): boolean {
+ throw new Error('This member is abstract.');
+ }
+
+ /**
+ * Navigates back.
+ */
+ public goBack() {
+ throw new Error('Method not implemented.');
+ }
+
+ /**
+ * Navigates forward.
+ */
+ public goForward() {
+ throw new Error('Method not implemented.');
+ }
+
+ /**
+ * Reloads the current url.
+ */
+ public reload() {
+ throw new Error('Method not implemented.');
+ }
+
+ [srcProperty.getDefault](): string {
+ return '';
+ }
+
+ [srcProperty.setNative](src: string) {
+ if (!src || this.tempSuspendSrcLoading) {
+ return;
+ }
+ const originSrc = src;
+
+ this.stopLoading();
+
+ // Add file:/// prefix for local files.
+ // They should be loaded with _loadUrl() method as it handles query params.
+ if (src.startsWith('~/')) {
+ src = `file://${knownFolders.currentApp().path}/${src.substr(2)}`;
+ this.writeTrace(`WebViewExt.src = "${originSrc}" startsWith ~/ resolved to "${src}"`);
+ } else if (src.startsWith('/')) {
+ src = `file://${src}`;
+ this.writeTrace(`WebViewExt.src = "${originSrc}" startsWith "/" resolved to ${src}`);
+ }
+
+ const lcSrc = src.toLowerCase();
+
+ // loading local files from paths with spaces may fail
+ if (lcSrc.startsWith('file:///')) {
+ src = encodeURI(src);
+ if (lcSrc !== src) {
+ this.writeTrace(`WebViewExt.src = "${originSrc}" escaped to "${src}"`);
+ }
+ }
+
+ if (lcSrc.startsWith(this.interceptScheme) || lcSrc.startsWith('http://') || lcSrc.startsWith('https://') || lcSrc.startsWith('file:///')) {
+ src = this.normalizeURL(src);
+
+ if (originSrc !== src) {
+ // Make sure the src-property reflects the actual value.
+ try {
+ this.tempSuspendSrcLoading = true;
+ this.src = src;
+ } catch {
+ // ignore
+ } finally {
+ this.tempSuspendSrcLoading = false;
+ }
+ }
+
+ this._loadUrl(src);
+
+ this.writeTrace(`WebViewExt.src = "${originSrc}" - LoadUrl("${src}")`);
+ } else {
+ this._loadData(src);
+ this.writeTrace(`WebViewExt.src = "${originSrc}" - LoadData("${src}")`);
+ }
+ }
+
+ [viewPortProperty.setNative](value: ViewPortProperties) {
+ if (this.src) {
+ this.injectViewPortMeta();
+ }
+ }
+
+ public resolveLocalResourceFilePath(filepath: string): string | void {
+ if (!filepath) {
+ this.writeTrace('WebViewExt.resolveLocalResourceFilePath() no filepath', Trace.messageType.error);
+
+ return;
+ }
+
+ if (filepath.startsWith('~')) {
+ filepath = path.normalize(knownFolders.currentApp().path + filepath.substr(1));
+ }
+
+ if (filepath.startsWith('file://')) {
+ filepath = filepath.replace(/^file:\/\//, '');
+ }
+
+ if (!File.exists(filepath)) {
+ this.writeTrace(`WebViewExt.resolveLocalResourceFilePath("${filepath}") - no such file`, Trace.messageType.error);
+
+ return;
+ }
+
+ return filepath;
+ }
+
+ /**
+ * Register a local resource.
+ * This resource can be loaded via "x-local://{name}" inside the webview
+ */
+ public registerLocalResource(name: string, filepath: string): void {
+ throw new Error('Method not implemented.');
+ }
+
+ /**
+ * Unregister a local resource.
+ */
+ public unregisterLocalResource(name: string): void {
+ throw new Error('Method not implemented.');
+ }
+
+ /**
+ * Resolve a "x-local://{name}" to file-path.
+ */
+ public getRegisteredLocalResource(name: string): string | void {
+ throw new Error('Method not implemented.');
+ }
+
+ /**
+ * Load URL - Wait for promise
+ *
+ * @param {string} src
+ * @returns {Promise}
+ */
+ public loadUrl(src: string): Promise {
+ if (!src) {
+ return this._onLoadFinished(src, 'empty src');
+ }
+
+ return new Promise((resolve, reject) => {
+ const loadFinishedEvent = (args: LoadFinishedEventData) => {
+ this.off(WebViewExtBase.loadFinishedEvent, loadFinishedEvent);
+ if (args.error) {
+ reject(args);
+ } else {
+ resolve(args);
+ }
+ };
+
+ this.on(WebViewExtBase.loadFinishedEvent, loadFinishedEvent);
+
+ this.src = src;
+ });
+ }
+
+ /**
+ * Load a JavaScript file on the current page in the webview.
+ */
+ public loadJavaScriptFile(scriptName: string, filepath: string) {
+ return this.loadJavaScriptFiles([
+ {
+ resourceName: scriptName,
+ filepath,
+ },
+ ]);
+ }
+
+ /**
+ * Load multiple JavaScript-files on the current page in the webview.
+ */
+ public async loadJavaScriptFiles(files: LoadStyleSheetResource[]) {
+ if (!files || !files.length) {
+ return;
+ }
+
+ const promiseScriptCodes = [] as Promise[];
+
+ for (const { resourceName, filepath } of files) {
+ const scriptCode = this.generateLoadJavaScriptFileScriptCode(resourceName, filepath);
+ promiseScriptCodes.push(scriptCode);
+ this.writeTrace(`WebViewExt.loadJavaScriptFiles() - > Loading javascript file: "${filepath}"`);
+ }
+
+ if (promiseScriptCodes.length !== files.length) {
+ this.writeTrace(`WebViewExt.loadJavaScriptFiles() - > Num of generated scriptCodes ${promiseScriptCodes.length} differ from num files ${files.length}`, Trace.messageType.error);
+ }
+
+ if (!promiseScriptCodes.length) {
+ this.writeTrace('WebViewExt.loadJavaScriptFiles() - > No files');
+
+ return;
+ }
+
+ if (!promiseScriptCodes.length) {
+ return;
+ }
+
+ await this.executePromises(await Promise.all(promiseScriptCodes));
+ }
+
+ /**
+ * Load a stylesheet file on the current page in the webview.
+ */
+ public loadStyleSheetFile(stylesheetName: string, filepath: string, insertBefore = true) {
+ return this.loadStyleSheetFiles([
+ {
+ resourceName: stylesheetName,
+ filepath,
+ insertBefore,
+ },
+ ]);
+ }
+
+ /**
+ * Load multiple stylesheet-files on the current page in the webview
+ */
+ public async loadStyleSheetFiles(files: LoadStyleSheetResource[]) {
+ if (!files || !files.length) {
+ return;
+ }
+
+ const promiseScriptCodes = [] as Promise[];
+
+ for (const { resourceName, filepath, insertBefore } of files) {
+ const scriptCode = this.generateLoadCSSFileScriptCode(resourceName, filepath, insertBefore);
+ promiseScriptCodes.push(scriptCode);
+ }
+
+ if (promiseScriptCodes.length !== files.length) {
+ this.writeTrace(`WebViewExt.loadStyleSheetFiles() - > Num of generated scriptCodes ${promiseScriptCodes.length} differ from num files ${files.length}`, Trace.messageType.error);
+ }
+
+ if (!promiseScriptCodes.length) {
+ this.writeTrace('WebViewExt.loadStyleSheetFiles() - > No files');
+
+ return;
+ }
+
+ await this.executePromises(await Promise.all(promiseScriptCodes));
+ }
+
+ /**
+ * Auto-load a JavaScript-file after the page have been loaded.
+ */
+ public autoLoadJavaScriptFile(resourceName: string, filepath: string) {
+ if (this.src) {
+ this.loadJavaScriptFile(resourceName, filepath).catch(() => void 0);
+ }
+
+ this.autoInjectScriptFiles.push({ resourceName, filepath });
+ }
+
+ public removeAutoLoadJavaScriptFile(resourceName: string) {
+ this.autoInjectScriptFiles = this.autoInjectScriptFiles.filter((data) => data.resourceName !== resourceName);
+ }
+
+ /**
+ * Auto-load a stylesheet-file after the page have been loaded.
+ */
+ public autoLoadStyleSheetFile(resourceName: string, filepath: string, insertBefore?: boolean) {
+ if (this.src) {
+ this.loadStyleSheetFile(resourceName, filepath, insertBefore).catch(() => void 0);
+ }
+
+ this.autoInjectStyleSheetFiles.push({
+ resourceName,
+ filepath,
+ insertBefore,
+ });
+ }
+
+ public removeAutoLoadStyleSheetFile(resourceName: string) {
+ this.autoInjectStyleSheetFiles = this.autoInjectStyleSheetFiles.filter((data) => data.resourceName !== resourceName);
+ }
+
+ public autoExecuteJavaScript(scriptCode: string, name: string) {
+ if (this.src) {
+ this.executePromise(scriptCode).catch(() => void 0);
+ }
+
+ this.removeAutoExecuteJavaScript(name);
+
+ const fixedCodeBlock = scriptCode.trim();
+ this.autoInjectJavaScriptBlocks.push({
+ scriptCode: fixedCodeBlock,
+ name,
+ });
+ }
+
+ public removeAutoExecuteJavaScript(name: string) {
+ this.autoInjectJavaScriptBlocks = this.autoInjectJavaScriptBlocks.filter((data) => data.name !== name);
+ }
+
+ public normalizeURL(url: string): string {
+ if (!url) {
+ return url;
+ }
+
+ if (url.startsWith(this.interceptScheme)) {
+ return url;
+ }
+
+ return new URL(url).href;
+ }
+
+ /**
+ * Ensure fetch-api is available.
+ */
+ protected async ensureFetchSupport(): Promise {
+ if (WebViewExtBase.isFetchSupported) {
+ return Promise.resolve();
+ }
+
+ if (typeof WebViewExtBase.isFetchSupported === 'undefined') {
+ this.writeTrace('WebViewExtBase.ensureFetchSupport() - need to check for fetch support.');
+
+ WebViewExtBase.isFetchSupported = await this.executeJavaScript("typeof fetch !== 'undefined'");
+ }
+
+ if (WebViewExtBase.isFetchSupported) {
+ this.writeTrace('WebViewExtBase.ensureFetchSupport() - fetch is supported - polyfill not needed.');
+
+ return;
+ }
+
+ this.writeTrace('WebViewExtBase.ensureFetchSupport() - fetch is not supported - polyfill needed.');
+
+ return await this.loadFetchPolyfill();
+ }
+
+ protected async loadFetchPolyfill() {
+ await this.executeJavaScript(fetchPolyfill, false);
+ }
+
+ /**
+ * Older Android WebView don't support promises.
+ * Inject the promise-polyfill if needed.
+ */
+ protected async ensurePromiseSupport() {
+ if (WebViewExtBase.isPromiseSupported) {
+ return;
+ }
+
+ if (typeof WebViewExtBase.isPromiseSupported === 'undefined') {
+ this.writeTrace('WebViewExtBase.ensurePromiseSupport() - need to check for promise support.');
+
+ WebViewExtBase.isPromiseSupported = await this.executeJavaScript("typeof Promise !== 'undefined'");
+ }
+
+ if (WebViewExtBase.isPromiseSupported) {
+ this.writeTrace('WebViewExtBase.ensurePromiseSupport() - promise is supported - polyfill not needed.');
+
+ return;
+ }
+
+ this.writeTrace('WebViewExtBase.ensurePromiseSupport() - promise is not supported - polyfill needed.');
+ await this.loadPromisePolyfill();
+ }
+
+ protected async loadPromisePolyfill() {
+ await this.executeJavaScript(promisePolyfill, false);
+ }
+
+ protected async ensurePolyfills() {
+ await this.ensurePromiseSupport();
+ await this.ensureFetchSupport();
+ }
+
+ /**
+ * Execute JavaScript inside the webview.
+ * The code should be wrapped inside an anonymous-function.
+ * Larger scripts should be injected with loadJavaScriptFile.
+ * NOTE: stringifyResult only applies on iOS.
+ */
+ public executeJavaScript(scriptCode: string, stringifyResult?: boolean): Promise {
+ throw new Error('Method not implemented.');
+ }
+
+ /**
+ * Execute a promise inside the webview and wait for it to resolve.
+ * Note: The scriptCode must return a promise.
+ */
+ public async executePromise(scriptCode: string, timeout = 2000): Promise {
+ const results = await this.executePromises([scriptCode], timeout);
+
+ return results && results[0];
+ }
+
+ public async executePromises(scriptCodes: string[], timeout = 2000): Promise {
+ if (scriptCodes.length === 0) {
+ return;
+ }
+
+ const reqId = `${Math.round(Math.random() * 1000)}`;
+ const eventName = `tmp-promise-event-${reqId}`;
+
+ const scriptHeader = `
+ var promises = [];
+ var p = Promise.resolve();
+ `.trim();
+
+ const scriptBody = [] as string[];
+
+ for (const scriptCode of scriptCodes) {
+ if (!scriptCode) {
+ continue;
+ }
+
+ if (typeof scriptCode !== 'string') {
+ this.writeTrace(`WebViewExt.executePromises() - scriptCode is not a string`);
+ continue;
+ }
+
+ // Wrapped in a Promise.then to delay executing scriptCode till the previous promise have finished
+ scriptBody.push(
+ `
+ p = p.then(function() {
+ return ${scriptCode.trim()};
+ });
+
+ promises.push(p);
+ `.trim()
+ );
+ }
+
+ const scriptFooter = `
+ return Promise.all(promises);
+ `.trim();
+
+ const scriptCode = `(function() {
+ ${scriptHeader}
+ ${scriptBody.join(';')}
+ ${scriptFooter}
+ })()`.trim();
+
+ const promiseScriptCode = `
+ (function() {
+ var eventName = ${JSON.stringify(eventName)};
+ try {
+ var promise = (function() {return ${scriptCode}})();
+ window.nsWebViewBridge.executePromise(promise, eventName);
+ } catch (err) {
+ window.nsWebViewBridge.emitError(err, eventName);
+ }
+ })();
+ `.trim();
+
+ return new Promise((resolve, reject) => {
+ let timer: any;
+ const tmpPromiseEvent = (args: any) => {
+ clearTimeout(timer);
+
+ const { data, err } = args.data || ({} as any);
+
+ // Was it a success? No 'err' received.
+ if (typeof err === 'undefined') {
+ resolve(data);
+
+ return;
+ }
+
+ // Rejected promise.
+ if (err && typeof err === 'object') {
+ // err is an object. Might be a serialized Error-object.
+ const error = new Error(err.message || err.name || err);
+ if (err.stack) {
+ // Add the web stack to the Error object.
+ (error as any).webStack = err.stack;
+ }
+
+ for (const [key, value] of Object.entries(err)) {
+ if (key in error) {
+ continue;
+ }
+
+ error[key] = value;
+ }
+
+ reject(error);
+
+ return;
+ }
+
+ reject(new Error(err));
+ };
+
+ this.once(eventName, tmpPromiseEvent);
+
+ this.executeJavaScript(promiseScriptCode, false);
+
+ if (timeout > 0) {
+ timer = setTimeout(() => {
+ reject(new Error(`Timed out after: ${timeout}`));
+
+ this.off(eventName);
+ }, timeout);
+ }
+ });
+ }
+
+ /**
+ * Generate script code for loading javascript-file.
+ */
+ public async generateLoadJavaScriptFileScriptCode(resourceName: string, path: string) {
+ if (this.supportXLocalScheme) {
+ const fixedResourceName = this.fixLocalResourceName(resourceName);
+ if (path) {
+ this.registerLocalResource(fixedResourceName, path);
+ }
+
+ const scriptHref = `${this.interceptScheme}://${fixedResourceName}`;
+
+ return `window.nsWebViewBridge.injectJavaScriptFile(${JSON.stringify(scriptHref)});`;
+ } else {
+ const elId = resourceName.replace(/^[:]*:\/\//, '').replace(/[^a-z0-9]/g, '');
+ const scriptCode = await File.fromPath(this.resolveLocalResourceFilePath(path) as string).readText();
+
+ return `window.nsWebViewBridge.injectJavaScript(${JSON.stringify(elId)}, ${scriptCode});`;
+ }
+ }
+
+ /**
+ * Generate script code for loading CSS-file.generateLoadCSSFileScriptCode
+ */
+ public async generateLoadCSSFileScriptCode(resourceName: string, path: string, insertBefore = false) {
+ if (this.supportXLocalScheme) {
+ resourceName = this.fixLocalResourceName(resourceName);
+ if (path) {
+ this.registerLocalResource(resourceName, path);
+ }
+
+ const stylesheetHref = `${this.interceptScheme}://${resourceName}`;
+
+ return `window.nsWebViewBridge.injectStyleSheetFile(${JSON.stringify(stylesheetHref)}, ${!!insertBefore});`;
+ } else {
+ const elId = resourceName.replace(/^[:]*:\/\//, '').replace(/[^a-z0-9]/g, '');
+
+ const stylesheetCode = await File.fromPath(this.resolveLocalResourceFilePath(path) as string).readText();
+
+ return `window.nsWebViewBridge.injectStyleSheet(${JSON.stringify(elId)}, ${JSON.stringify(stylesheetCode)}, ${!!insertBefore})`;
+ }
+ }
+
+ /**
+ * Inject WebView JavaScript Bridge.
+ */
+ protected async injectWebViewBridge(): Promise {
+ await this.executeJavaScript(webViewBridge, false);
+ await this.ensurePolyfills();
+ await this.injectViewPortMeta();
+ }
+
+ protected async injectViewPortMeta(): Promise {
+ const scriptCode = await this.generateViewPortCode();
+ if (!scriptCode) {
+ return;
+ }
+
+ await this.executeJavaScript(scriptCode, false);
+ }
+
+ public async generateViewPortCode(): Promise {
+ if (this.viewPortSize === false) {
+ return null;
+ }
+
+ const scriptCodeTmpl = metadataViewPort;
+
+ const viewPortCode = JSON.stringify(this.viewPortSize || {});
+
+ return scriptCodeTmpl.replace('"<%= VIEW_PORT %>"', viewPortCode);
+ }
+
+ /**
+ * Convert response from WebView into usable JS-type.
+ */
+ protected parseWebViewJavascriptResult(result: any) {
+ if (result === undefined) {
+ return;
+ }
+
+ if (typeof result !== 'string') {
+ return result;
+ }
+
+ try {
+ return JSON.parse(result);
+ } catch (err) {
+ return result;
+ }
+ }
+
+ public writeTrace(message: string, type = Trace.messageType.info) {
+ if (Trace.isEnabled()) {
+ Trace.write(message, 'NOTA', type);
+ }
+ }
+
+ /**
+ * Emit event into the webview.
+ */
+ public emitToWebView(eventName: string, data: any) {
+ const scriptCode = `
+ window.nsWebViewBridge && nsWebViewBridge.onNativeEvent(${JSON.stringify(eventName)}, ${JSON.stringify(data)});
+ `;
+
+ this.executeJavaScript(scriptCode, false);
+ }
+
+ /**
+ * Called from delegate on webview event.
+ * Triggered by: window.nsWebViewBridge.emit(eventName: string, data: any); inside the webview
+ */
+ public onWebViewEvent(eventName: string, data: any) {
+ this.notify({
+ eventName,
+ object: this,
+ data,
+ });
+ }
+
+ /**
+ * Get document.title
+ * NOTE: On Android, if empty returns filename
+ */
+ public getTitle(): Promise {
+ throw new Error('Method not implemented.');
+ }
+
+ public zoomIn(): boolean {
+ throw new Error('Method not implemented.');
+ }
+
+ public zoomOut(): boolean {
+ throw new Error('Method not implemented.');
+ }
+
+ public zoomBy(zoomFactor: number) {
+ throw new Error('Method not implemented.');
+ }
+
+ /**
+ * Helper function, strips 'x-local://' from a resource name
+ */
+ public fixLocalResourceName(resourceName: string) {
+ if (resourceName.startsWith(this.interceptScheme)) {
+ return resourceName.substr(this.interceptScheme.length + 3);
+ }
+
+ return resourceName;
+ }
+
+ [isEnabledProperty.getDefault]() {
+ return true;
+ }
+}
+
+export interface WebViewExtBase {
+ /**
+ * A basic method signature to hook an event listener (shortcut alias to the addEventListener method).
+ * @param eventNames - String corresponding to events (e.g. "propertyChange"). Optionally could be used more events separated by `,` (e.g. "propertyChange", "change").
+ * @param callback - Callback function which will be executed when event is raised.
+ * @param thisArg - An optional parameter which will be used as `this` context for callback execution.
+ */
+ on(eventNames: string, callback: (data: WebViewEventData) => void, thisArg?: any);
+ once(eventNames: string, callback: (data: WebViewEventData) => void, thisArg?: any);
+
+ /**
+ * Raised before the webview requests an URL.
+ * Can be cancelled by settings args.cancel = true in your event handler.
+ */
+ on(event: EventNames.ShouldOverrideUrlLoading, callback: (args: ShouldOverrideUrlLoadEventData) => void, thisArg?: any);
+ once(event: EventNames.ShouldOverrideUrlLoading, callback: (args: ShouldOverrideUrlLoadEventData) => void, thisArg?: any);
+
+ /**
+ * Raised when a loadStarted event occurs.
+ */
+ on(event: EventNames.LoadStarted, callback: (args: LoadStartedEventData) => void, thisArg?: any);
+ once(event: EventNames.LoadStarted, callback: (args: LoadStartedEventData) => void, thisArg?: any);
+
+ /**
+ * Raised when a loadFinished event occurs.
+ */
+ on(event: EventNames.LoadFinished, callback: (args: LoadFinishedEventData) => void, thisArg?: any);
+ once(event: EventNames.LoadFinished, callback: (args: LoadFinishedEventData) => void, thisArg?: any);
+
+ /**
+ * Raised when a loadProgress event occurs.
+ */
+ on(event: EventNames.LoadProgress, callback: (args: LoadProgressEventData) => void, thisArg?: any);
+ once(event: EventNames.LoadProgress, callback: (args: LoadProgressEventData) => void, thisArg?: any);
+
+ /**
+ * Raised when a titleChanged event occurs.
+ */
+ on(event: EventNames.TitleChanged, callback: (args: TitleChangedEventData) => void, thisArg?: any);
+ once(event: EventNames.TitleChanged, callback: (args: TitleChangedEventData) => void, thisArg?: any);
+
+ /**
+ * Override web alerts to replace them.
+ * Call args.cancel() on close.
+ */
+ on(event: EventNames.WebAlert, callback: (args: WebAlertEventData) => void, thisArg?: any);
+ once(event: EventNames.WebAlert, callback: (args: WebAlertEventData) => void, thisArg?: any);
+
+ /**
+ * Override web confirm dialogs to replace them.
+ * Call args.cancel(res) on close.
+ */
+ on(event: EventNames.WebConfirm, callback: (args: WebConfirmEventData) => void, thisArg?: any);
+ once(event: EventNames.WebConfirm, callback: (args: WebConfirmEventData) => void, thisArg?: any);
+
+ /**
+ * Override web confirm prompts to replace them.
+ * Call args.cancel(res) on close.
+ */
+ on(event: EventNames.WebPrompt, callback: (args: WebPromptEventData) => void, thisArg?: any);
+ once(event: EventNames.WebPrompt, callback: (args: WebPromptEventData) => void, thisArg?: any);
+
+ /**
+ * Get Android WebView console entries.
+ */
+ on(event: EventNames.WebConsole, callback: (args: WebConsoleEventData) => void, thisArg?: any);
+ once(event: EventNames.WebConsole, callback: (args: WebConsoleEventData) => void, thisArg?: any);
+}
+
+autoInjectJSBridgeProperty.register(WebViewExtBase);
+builtInZoomControlsProperty.register(WebViewExtBase);
+cacheModeProperty.register(WebViewExtBase);
+databaseStorageProperty.register(WebViewExtBase);
+debugModeProperty.register(WebViewExtBase);
+displayZoomControlsProperty.register(WebViewExtBase);
+domStorageProperty.register(WebViewExtBase);
+srcProperty.register(WebViewExtBase);
+supportZoomProperty.register(WebViewExtBase);
+scrollBounceProperty.register(WebViewExtBase);
+viewPortProperty.register(WebViewExtBase);
+limitsNavigationsToAppBoundDomainsProperty.register(WebViewExtBase);
diff --git a/packages/nativescript-webview-ext/index.android.ts b/packages/nativescript-webview-ext/index.android.ts
new file mode 100644
index 0000000..93dbe03
--- /dev/null
+++ b/packages/nativescript-webview-ext/index.android.ts
@@ -0,0 +1,893 @@
+import '@nativescript/core';
+import { File, knownFolders, Trace } from '@nativescript/core';
+import { isEnabledProperty } from '@nativescript/core/ui/core/view';
+import { builtInZoomControlsProperty, CacheMode, cacheModeProperty, databaseStorageProperty, debugModeProperty, displayZoomControlsProperty, domStorageProperty, supportZoomProperty, UnsupportedSDKError, WebViewExtBase } from './common';
+
+export * from './common';
+
+const extToMimeType = new Map([
+ ['html', 'text/html'],
+ ['htm', 'text/html'],
+ ['xhtml', 'text/html'],
+ ['xhtm', 'text/html'],
+ ['css', 'text/css'],
+ ['gif', 'image/gif'],
+ ['jpeg', 'image/jpeg'],
+ ['jpg', 'image/jpeg'],
+ ['js', 'text/javascript'],
+ ['otf', 'application/vnd.ms-opentype'],
+ ['png', 'image/png'],
+ ['svg', 'image/svg+xml'],
+ ['ttf', 'application/x-font-ttf'],
+]);
+
+const extToBinaryEncoding = new Set(['gif', 'jpeg', 'jpg', 'otf', 'png', 'ttf']);
+
+//#region android_native_classes
+let cacheModeMap: Map;
+
+export interface AndroidWebViewClient extends android.webkit.WebViewClient {}
+
+export interface AndroidWebView extends android.webkit.WebView {
+ client: AndroidWebViewClient | null;
+ chromeClient: android.webkit.WebChromeClient | null;
+ bridgeInterface?: dk.nota.webviewinterface.WebViewBridgeInterface;
+}
+
+let WebViewExtClient: new (owner: WebViewExt) => AndroidWebViewClient;
+let WebChromeViewExtClient: new (owner: WebViewExt) => android.webkit.WebChromeClient;
+let WebViewBridgeInterface: new (owner: WebViewExt) => dk.nota.webviewinterface.WebViewBridgeInterface;
+
+function initializeWebViewClient(): void {
+ if (WebViewExtClient) {
+ return;
+ }
+
+ cacheModeMap = new Map([
+ ['cache_first', android.webkit.WebSettings.LOAD_CACHE_ELSE_NETWORK],
+ ['cache_only', android.webkit.WebSettings.LOAD_CACHE_ONLY],
+ ['default', android.webkit.WebSettings.LOAD_DEFAULT],
+ ['no_cache', android.webkit.WebSettings.LOAD_NO_CACHE],
+ ['normal', android.webkit.WebSettings.LOAD_NORMAL],
+ ]);
+
+ @NativeClass()
+ class WebViewExtClientImpl extends android.webkit.WebViewClient {
+ private owner: WeakRef;
+ constructor(owner: WebViewExt) {
+ super();
+
+ this.owner = new WeakRef(owner);
+
+ return global.__native(this);
+ }
+
+ /**
+ * Give the host application a chance to take control when a URL is about to be loaded in the current WebView.
+ */
+ public shouldOverrideUrlLoading(view: android.webkit.WebView, request: string | android.webkit.WebResourceRequest) {
+ const owner = this.owner.get();
+ if (!owner) {
+ console.warn('WebViewExtClientImpl.shouldOverrideUrlLoading(...) - no owner');
+
+ return true;
+ }
+
+ let url = request as string;
+ let httpMethod = 'GET';
+ let isRedirect = false;
+ let hasGesture = false;
+ let isForMainFrame = false;
+ let requestHeaders: java.util.Map | null = null;
+ if (typeof request === 'object') {
+ httpMethod = request.getMethod();
+ isRedirect = request.isRedirect();
+ hasGesture = request.hasGesture();
+ isForMainFrame = request.isForMainFrame();
+ requestHeaders = request.getRequestHeaders();
+
+ url = request.getUrl().toString();
+ }
+
+ owner.writeTrace(`WebViewClientClass.shouldOverrideUrlLoading("${url}") - method:${httpMethod} isRedirect:${isRedirect} hasGesture:${hasGesture} isForMainFrame:${isForMainFrame} headers:${requestHeaders}`);
+
+ if (url.startsWith(owner.interceptScheme)) {
+ owner.writeTrace(`WebViewClientClass.shouldOverrideUrlLoading("${url}") - "${owner.interceptScheme}" - cancel`);
+
+ return true;
+ }
+
+ const shouldOverrideUrlLoading = owner._onShouldOverrideUrlLoading(url, httpMethod);
+ if (shouldOverrideUrlLoading === true) {
+ owner.writeTrace(`WebViewClientClass.shouldOverrideUrlLoading("${url}") - cancel loading url`);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public shouldInterceptRequest(view: android.webkit.WebView, request: string | android.webkit.WebResourceRequest) {
+ const owner = this.owner.get();
+ if (!owner) {
+ console.warn('WebViewExtClientImpl.shouldInterceptRequest(...) - no owner');
+
+ return super.shouldInterceptRequest(view, request as android.webkit.WebResourceRequest);
+ }
+
+ let url: string | void;
+ if (typeof request === 'string') {
+ url = request;
+ } else if (typeof request === 'object') {
+ url = request.getUrl().toString();
+ }
+
+ if (typeof url !== 'string') {
+ owner.writeTrace(`WebViewClientClass.shouldInterceptRequest("${url}") - is not a string`);
+
+ return super.shouldInterceptRequest(view, request as android.webkit.WebResourceRequest);
+ }
+
+ if (!url.startsWith(owner.interceptScheme)) {
+ return super.shouldInterceptRequest(view, request as android.webkit.WebResourceRequest);
+ }
+
+ const filepath = owner.getRegisteredLocalResource(url);
+ if (!filepath) {
+ owner.writeTrace(`WebViewClientClass.shouldInterceptRequest("${url}") - no matching file`);
+
+ return super.shouldInterceptRequest(view, request as android.webkit.WebResourceRequest);
+ }
+
+ if (!File.exists(filepath)) {
+ owner.writeTrace(`WebViewClientClass.shouldInterceptRequest("${url}") - file: "${filepath}" doesn't exists`);
+
+ return super.shouldInterceptRequest(view, request as android.webkit.WebResourceRequest);
+ }
+
+ const tnsFile = File.fromPath(filepath);
+
+ const javaFile = new java.io.File(tnsFile.path);
+ const stream = new java.io.FileInputStream(javaFile);
+ const ext = tnsFile.extension.substr(1).toLowerCase();
+ const mimeType = extToMimeType.get(ext) || 'application/octet-stream';
+ const encoding = extToBinaryEncoding.has(ext) || mimeType === 'application/octet-stream' ? 'binary' : 'UTF-8';
+
+ owner.writeTrace(`WebViewClientClass.shouldInterceptRequest("${url}") - file: "${filepath}" mimeType:${mimeType} encoding:${encoding}`);
+
+ const response = new android.webkit.WebResourceResponse(mimeType, encoding, stream);
+ if (android.os.Build.VERSION.SDK_INT < 21 || !response.getResponseHeaders) {
+ return response;
+ }
+
+ let responseHeaders = response.getResponseHeaders();
+ if (!responseHeaders) {
+ responseHeaders = new java.util.HashMap();
+ }
+
+ responseHeaders.put('Access-Control-Allow-Origin', '*');
+ response.setResponseHeaders(responseHeaders);
+
+ return response;
+ }
+
+ public onPageStarted(view: android.webkit.WebView, url: string, favicon: android.graphics.Bitmap) {
+ super.onPageStarted(view, url, favicon);
+ const owner = this.owner.get();
+ if (!owner) {
+ console.warn(`WebViewExtClientImpl.onPageStarted("${view}", "${url}", "${favicon}") - no owner`);
+
+ return;
+ }
+
+ owner.writeTrace(`WebViewClientClass.onPageStarted("${view}", "${url}", "${favicon}")`);
+ owner._onLoadStarted(url);
+ }
+
+ public onPageFinished(view: android.webkit.WebView, url: string) {
+ super.onPageFinished(view, url);
+
+ const owner = this.owner.get();
+ if (!owner) {
+ console.warn(`WebViewExtClientImpl.onPageFinished("${view}", ${url}") - no owner`);
+
+ return;
+ }
+
+ owner.writeTrace(`WebViewClientClass.onPageFinished("${view}", ${url}")`);
+ owner._onLoadFinished(url).catch(() => void 0);
+ }
+
+ public onReceivedError(...args: any[]) {
+ if (args.length === 4) {
+ const [view, errorCode, description, failingUrl] = args as [android.webkit.WebView, number, string, string];
+ this.onReceivedErrorBeforeAPI23(view, errorCode, description, failingUrl);
+ } else {
+ const [view, request, error] = args as [android.webkit.WebView, any, any];
+ this.onReceivedErrorAPI23(view, request, error);
+ }
+ }
+
+ private onReceivedErrorAPI23(view: android.webkit.WebView, request: any, error: any) {
+ super.onReceivedError(view, request, error);
+
+ const owner = this.owner.get();
+ if (!owner) {
+ console.warn('WebViewExtClientImpl.onReceivedErrorAPI23(...) - no owner');
+
+ return;
+ }
+
+ let url = error.getUrl && error.getUrl();
+ if (!url && typeof request === 'object') {
+ url = request.getUrl().toString();
+ }
+
+ owner.writeTrace(`WebViewClientClass.onReceivedErrorAPI23(${error.getErrorCode()}, ${error.getDescription()}, ${url})`);
+
+ owner._onLoadFinished(url, `${error.getDescription()}(${error.getErrorCode()})`).catch(() => void 0);
+ }
+
+ private onReceivedErrorBeforeAPI23(view: android.webkit.WebView, errorCode: number, description: string, failingUrl: string) {
+ super.onReceivedError(view, errorCode, description, failingUrl);
+
+ const owner = this.owner.get();
+ if (!owner) {
+ console.warn('WebViewExtClientImpl.onReceivedErrorBeforeAPI23(...) - no owner');
+
+ return;
+ }
+
+ owner.writeTrace(`WebViewClientClass.onReceivedErrorBeforeAPI23(${errorCode}, "${description}", "${failingUrl}")`);
+ owner._onLoadFinished(failingUrl, `${description}(${errorCode})`).catch(() => void 0);
+ }
+ }
+
+ WebViewExtClient = WebViewExtClientImpl;
+
+ @NativeClass()
+ class WebChromeViewExtClientImpl extends android.webkit.WebChromeClient {
+ private owner: WeakRef;
+ private showCustomViewCallback?: android.webkit.WebChromeClient.CustomViewCallback;
+
+ constructor(owner: WebViewExt) {
+ super();
+
+ this.owner = new WeakRef(owner);
+
+ return global.__native(this);
+ }
+
+ public onShowCustomView(view: AndroidWebView) {
+ const owner = this.owner.get();
+ if (!owner) {
+ return;
+ }
+
+ let callback: android.webkit.WebChromeClient.CustomViewCallback;
+
+ if (arguments.length === 3) {
+ callback = arguments[2];
+ } else if (arguments.length === 2) {
+ callback = arguments[1];
+ } else {
+ return;
+ }
+
+ if (owner._onEnterFullscreen(() => this.hideCustomView())) {
+ this.showCustomViewCallback = callback;
+ } else {
+ callback.onCustomViewHidden();
+ }
+ }
+
+ private hideCustomView() {
+ if (this.showCustomViewCallback) {
+ this.showCustomViewCallback.onCustomViewHidden();
+ }
+
+ this.showCustomViewCallback = undefined;
+ }
+
+ public onHideCustomView() {
+ this.showCustomViewCallback = undefined;
+
+ const owner = this.owner.get();
+ if (!owner) {
+ return;
+ }
+
+ owner._onExitFullscreen();
+ }
+
+ public onProgressChanged(view: AndroidWebView, newProgress: number) {
+ const owner = this.owner.get();
+ if (!owner) {
+ return;
+ }
+
+ owner._loadProgress(newProgress);
+ }
+
+ public onReceivedTitle(view: AndroidWebView, title: string) {
+ const owner = this.owner.get();
+ if (!owner) {
+ return;
+ }
+
+ owner._titleChanged(title);
+ }
+
+ public onJsAlert(view: AndroidWebView, url: string, message: string, result: android.webkit.JsResult): boolean {
+ const owner = this.owner.get();
+ if (!owner) {
+ return false;
+ }
+
+ let gotResponse = false;
+
+ return owner._webAlert(message, () => {
+ if (!gotResponse) {
+ result.confirm();
+ }
+
+ gotResponse = true;
+ });
+ }
+
+ public onJsConfirm(view: AndroidWebView, url: string, message: string, result: android.webkit.JsResult): boolean {
+ const owner = this.owner.get();
+ if (!owner) {
+ return false;
+ }
+
+ let gotResponse = false;
+
+ return owner._webConfirm(message, (confirmed: boolean) => {
+ if (!gotResponse) {
+ if (confirmed) {
+ result.confirm();
+ } else {
+ result.cancel();
+ }
+ }
+
+ gotResponse = true;
+ });
+ }
+
+ public onJsPrompt(view: AndroidWebView, url: string, message: string, defaultValue: string, result: android.webkit.JsPromptResult): boolean {
+ const owner = this.owner.get();
+ if (!owner) {
+ return false;
+ }
+
+ let gotResponse = false;
+
+ return owner._webPrompt(message, defaultValue, (message: string) => {
+ if (!gotResponse) {
+ if (message) {
+ result.confirm(message);
+ } else {
+ result.confirm();
+ }
+ }
+
+ gotResponse = true;
+ });
+ }
+
+ public onConsoleMessage(): boolean {
+ if (arguments.length !== 1) {
+ return false;
+ }
+
+ const owner = this.owner.get();
+ if (!owner) {
+ return false;
+ }
+
+ const consoleMessage = arguments[0] as android.webkit.ConsoleMessage;
+
+ if (consoleMessage instanceof android.webkit.ConsoleMessage) {
+ const message = consoleMessage.message();
+ const lineNo = consoleMessage.lineNumber();
+ let level = 'log';
+ const { DEBUG, LOG, WARNING } = android.webkit.ConsoleMessage.MessageLevel;
+ switch (consoleMessage.messageLevel()) {
+ case DEBUG: {
+ level = 'debug';
+ break;
+ }
+ case LOG: {
+ level = 'log';
+ break;
+ }
+ case WARNING: {
+ level = 'warn';
+ break;
+ }
+ }
+
+ return owner._webConsole(message, lineNo, level);
+ }
+
+ return false;
+ }
+ }
+
+ WebChromeViewExtClient = WebChromeViewExtClientImpl;
+
+ @NativeClass()
+ class WebViewBridgeInterfaceImpl extends dk.nota.webviewinterface.WebViewBridgeInterface {
+ private owner: WeakRef;
+ constructor(owner: WebViewExt) {
+ super();
+
+ this.owner = new WeakRef(owner);
+
+ return global.__native(this);
+ }
+
+ public emitEventToNativeScript(eventName: string, data: string) {
+ const owner = this.owner.get();
+ if (!owner) {
+ console.warn(`WebViewExtClientImpl.emitEventToNativeScript("${eventName}") - no owner`);
+
+ return;
+ }
+
+ try {
+ if (typeof data == 'string' && data) {
+ owner.onWebViewEvent(eventName, JSON.parse(data));
+ return;
+ }
+
+ owner.onWebViewEvent(eventName, null);
+
+ return;
+ } catch (err) {
+ owner.writeTrace(`WebViewExtClientImpl.emitEventToNativeScript("${eventName}") - couldn't parse data: ${JSON.stringify(data)} err: ${err}`);
+ }
+ }
+ }
+
+ WebViewBridgeInterface = WebViewBridgeInterfaceImpl;
+}
+//#endregion android_native_classes
+
+let instanceNo = 0;
+export class WebViewExt extends WebViewExtBase {
+ public static supportXLocalScheme = true;
+
+ public nativeViewProtected: AndroidWebView | void;
+
+ protected readonly localResourceMap = new Map();
+
+ public supportXLocalScheme = true;
+
+ public readonly instance = ++instanceNo;
+
+ public android: AndroidWebView;
+
+ public createNativeView() {
+ const nativeView = new android.webkit.WebView(this._context) as AndroidWebView;
+ const settings = nativeView.getSettings();
+
+ // Needed for the bridge library
+ settings.setJavaScriptEnabled(true);
+
+ settings.setAllowFileAccess(true); // Needed for Android 11
+
+ settings.setBuiltInZoomControls(!!this.builtInZoomControls);
+ settings.setDisplayZoomControls(!!this.displayZoomControls);
+ settings.setSupportZoom(!!this.supportZoom);
+
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ // Needed for x-local in https-sites
+ settings.setMixedContentMode(android.webkit.WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
+ }
+
+ // Needed for XHRRequests with x-local://
+ settings.setAllowUniversalAccessFromFileURLs(true);
+
+ return nativeView;
+ }
+
+ public initNativeView() {
+ super.initNativeView();
+
+ initializeWebViewClient();
+
+ const nativeView = this.nativeViewProtected;
+ if (!nativeView) {
+ return;
+ }
+
+ const client = new WebViewExtClient(this);
+ const chromeClient = new WebChromeViewExtClient(this);
+ nativeView.setWebViewClient(client);
+ nativeView.client = client;
+
+ nativeView.setWebChromeClient(chromeClient);
+ nativeView.chromeClient = chromeClient;
+
+ const bridgeInterface = new WebViewBridgeInterface(this);
+ nativeView.addJavascriptInterface(bridgeInterface, 'androidWebViewBridge');
+ nativeView.bridgeInterface = bridgeInterface;
+ }
+
+ public disposeNativeView() {
+ const nativeView = this.nativeViewProtected;
+ if (nativeView) {
+ nativeView.client = null;
+ nativeView.chromeClient = null;
+ nativeView.destroy();
+ }
+
+ super.disposeNativeView();
+ }
+
+ public async ensurePromiseSupport() {
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ return;
+ }
+
+ return await super.ensurePromiseSupport();
+ }
+
+ public _loadUrl(src: string) {
+ const nativeView = this.nativeViewProtected;
+ if (!nativeView) {
+ return;
+ }
+
+ this.writeTrace(`WebViewExt._loadUrl("${src}")`);
+ nativeView.loadUrl(src);
+ this.writeTrace(`WebViewExt._loadUrl("${src}") - end`);
+ }
+
+ public _loadData(src: string) {
+ const nativeView = this.nativeViewProtected;
+ if (!nativeView) {
+ return;
+ }
+
+ const baseUrl = `file:///${knownFolders.currentApp().path}/`;
+ this.writeTrace(`WebViewExt._loadData("${src}") -> baseUrl: "${baseUrl}"`);
+ nativeView.loadDataWithBaseURL(baseUrl, src, 'text/html', 'utf-8', null!);
+ }
+
+ public get canGoBack(): boolean {
+ const nativeView = this.nativeViewProtected;
+ if (nativeView) {
+ return nativeView.canGoBack();
+ }
+
+ return false;
+ }
+
+ public stopLoading() {
+ const nativeView = this.nativeViewProtected;
+ if (nativeView) {
+ nativeView.stopLoading();
+ }
+ }
+
+ get canGoForward(): boolean {
+ const nativeView = this.nativeViewProtected;
+ if (nativeView) {
+ return nativeView.canGoForward();
+ }
+
+ return false;
+ }
+
+ public goBack() {
+ const nativeView = this.nativeViewProtected;
+ if (nativeView) {
+ return nativeView.goBack();
+ }
+ }
+
+ public goForward() {
+ const nativeView = this.nativeViewProtected;
+ if (nativeView) {
+ return nativeView.goForward();
+ }
+ }
+
+ public reload() {
+ const nativeView = this.nativeViewProtected;
+ if (nativeView) {
+ return nativeView.reload();
+ }
+ }
+
+ public registerLocalResource(resourceName: string, path: string) {
+ resourceName = this.fixLocalResourceName(resourceName);
+
+ const filepath = this.resolveLocalResourceFilePath(path);
+ if (!filepath) {
+ this.writeTrace(`WebViewExt.registerLocalResource("${resourceName}", "${path}") -> file doesn't exist`, Trace.messageType.error);
+
+ return;
+ }
+
+ this.writeTrace(`WebViewExt.registerLocalResource("${resourceName}", "${path}") -> file: "${filepath}"`);
+
+ this.localResourceMap.set(resourceName, filepath);
+ }
+
+ public unregisterLocalResource(resourceName: string) {
+ this.writeTrace(`WebViewExt.unregisterLocalResource("${resourceName}")`);
+ resourceName = this.fixLocalResourceName(resourceName);
+
+ this.localResourceMap.delete(resourceName);
+ }
+
+ public getRegisteredLocalResource(resourceName: string) {
+ resourceName = this.fixLocalResourceName(resourceName);
+
+ const result = this.localResourceMap.get(resourceName);
+
+ this.writeTrace(`WebViewExt.getRegisteredLocalResource("${resourceName}") => "${result}"`);
+
+ return result;
+ }
+
+ /**
+ * Always load the Fetch-polyfill on Android.
+ *
+ * Native 'Fetch API' on Android rejects all request for resources no HTTP or HTTPS.
+ * This breaks x-local:// requests (and file://).
+ */
+ public async ensureFetchSupport() {
+ this.writeTrace("WebViewExt.ensureFetchSupport() - Override 'Fetch API' to support x-local.");
+
+ // The polyfill is not loaded if fetch already exists, start by null'ing it.
+ await this.executeJavaScript(
+ `
+ try {
+ window.fetch = null;
+ } catch (err) {
+ console.error("null'ing Native Fetch API failed:", err);
+ }
+ `
+ );
+
+ await this.loadFetchPolyfill();
+ }
+
+ public async executeJavaScript(scriptCode: string): Promise {
+ if (android.os.Build.VERSION.SDK_INT < 19) {
+ this.writeTrace(`WebViewExt.executeJavaScript() -> SDK:${android.os.Build.VERSION.SDK_INT} not supported`, Trace.messageType.error);
+
+ return Promise.reject(new UnsupportedSDKError(19));
+ }
+
+ const result = await new Promise((resolve, reject) => {
+ const androidWebView = this.nativeViewProtected;
+ if (!androidWebView) {
+ this.writeTrace(`WebViewExt.executeJavaScript() -> no nativeView?`, Trace.messageType.error);
+ reject(new Error('Native Android not initialized, cannot call executeJavaScript'));
+
+ return;
+ }
+
+ androidWebView.evaluateJavascript(
+ scriptCode,
+ new android.webkit.ValueCallback({
+ onReceiveValue(result: any) {
+ resolve(result);
+ },
+ })
+ );
+ });
+
+ return await this.parseWebViewJavascriptResult(result);
+ }
+
+ public async getTitle() {
+ return this.nativeViewProtected && this.nativeViewProtected.getTitle();
+ }
+
+ public zoomIn() {
+ const androidWebView = this.nativeViewProtected;
+ if (!androidWebView) {
+ return false;
+ }
+
+ return androidWebView.zoomIn();
+ }
+
+ public zoomOut() {
+ const androidWebView = this.nativeViewProtected;
+ if (!androidWebView) {
+ return false;
+ }
+
+ return androidWebView.zoomOut();
+ }
+
+ public zoomBy(zoomFactor: number) {
+ if (android.os.Build.VERSION.SDK_INT < 21) {
+ this.writeTrace(`WebViewExt.zoomBy - not supported on this SDK`);
+
+ return;
+ }
+
+ if (!this.nativeViewProtected) {
+ return;
+ }
+
+ if (zoomFactor >= 0.01 && zoomFactor <= 100) {
+ return this.nativeViewProtected.zoomBy(zoomFactor);
+ }
+
+ throw new Error(`ZoomBy only accepts values between 0.01 and 100 both inclusive`);
+ }
+
+ [debugModeProperty.getDefault]() {
+ return false;
+ }
+
+ [debugModeProperty.setNative](enabled: boolean) {
+ android.webkit.WebView.setWebContentsDebuggingEnabled(!!enabled);
+ }
+
+ [builtInZoomControlsProperty.getDefault]() {
+ const androidWebView = this.nativeViewProtected;
+ if (!androidWebView) {
+ return false;
+ }
+
+ const settings = androidWebView.getSettings();
+
+ return settings.getBuiltInZoomControls();
+ }
+
+ [builtInZoomControlsProperty.setNative](enabled: boolean) {
+ const androidWebView = this.nativeViewProtected;
+ if (!androidWebView) {
+ return;
+ }
+ const settings = androidWebView.getSettings();
+ settings.setBuiltInZoomControls(!!enabled);
+ }
+
+ [displayZoomControlsProperty.getDefault]() {
+ const androidWebView = this.nativeViewProtected;
+ if (!androidWebView) {
+ return false;
+ }
+
+ const settings = androidWebView.getSettings();
+
+ return settings.getDisplayZoomControls();
+ }
+
+ [displayZoomControlsProperty.setNative](enabled: boolean) {
+ const androidWebView = this.nativeViewProtected;
+ if (!androidWebView) {
+ return;
+ }
+ const settings = androidWebView.getSettings();
+ settings.setDisplayZoomControls(!!enabled);
+ }
+
+ [cacheModeProperty.getDefault](): CacheMode | null {
+ const androidWebView = this.nativeViewProtected;
+ if (!androidWebView) {
+ return null;
+ }
+
+ const settings = androidWebView.getSettings();
+ const cacheModeInt = settings.getCacheMode();
+ for (const [key, value] of cacheModeMap) {
+ if (value === cacheModeInt) {
+ return key;
+ }
+ }
+
+ return null;
+ }
+
+ [cacheModeProperty.setNative](cacheMode: CacheMode) {
+ const androidWebView = this.nativeViewProtected;
+ if (!androidWebView) {
+ return;
+ }
+
+ const settings = androidWebView.getSettings();
+ for (const [key, nativeValue] of cacheModeMap) {
+ if (key === cacheMode) {
+ settings.setCacheMode(nativeValue);
+
+ return;
+ }
+ }
+ }
+
+ [databaseStorageProperty.getDefault]() {
+ const androidWebView = this.nativeViewProtected;
+ if (!androidWebView) {
+ return false;
+ }
+
+ const settings = androidWebView.getSettings();
+
+ return settings.getDatabaseEnabled();
+ }
+
+ [databaseStorageProperty.setNative](enabled: boolean) {
+ const androidWebView = this.nativeViewProtected;
+ if (!androidWebView) {
+ return;
+ }
+
+ const settings = androidWebView.getSettings();
+ settings.setDatabaseEnabled(!!enabled);
+ }
+
+ [domStorageProperty.getDefault]() {
+ const androidWebView = this.nativeViewProtected;
+ if (!androidWebView) {
+ return false;
+ }
+
+ const settings = androidWebView.getSettings();
+
+ return settings.getDomStorageEnabled();
+ }
+
+ [domStorageProperty.setNative](enabled: boolean) {
+ const androidWebView = this.nativeViewProtected;
+ if (!androidWebView) {
+ return;
+ }
+
+ const settings = androidWebView.getSettings();
+ settings.setDomStorageEnabled(!!enabled);
+ }
+
+ [supportZoomProperty.getDefault]() {
+ const androidWebView = this.nativeViewProtected;
+ if (!androidWebView) {
+ return false;
+ }
+
+ const settings = androidWebView.getSettings();
+
+ return settings.supportZoom();
+ }
+
+ [supportZoomProperty.setNative](enabled: boolean) {
+ const androidWebView = this.nativeViewProtected;
+ if (!androidWebView) {
+ return;
+ }
+
+ const settings = androidWebView.getSettings();
+ settings.setSupportZoom(!!enabled);
+ }
+
+ [isEnabledProperty.setNative](enabled: boolean) {
+ const androidWebView = this.nativeViewProtected;
+ if (!androidWebView) {
+ return;
+ }
+
+ if (enabled) {
+ androidWebView.setOnTouchListener(null!);
+ } else {
+ androidWebView.setOnTouchListener(
+ new android.view.View.OnTouchListener({
+ onTouch() {
+ return true;
+ },
+ })
+ );
+ }
+ }
+}
diff --git a/packages/nativescript-webview-ext/index.d.ts b/packages/nativescript-webview-ext/index.d.ts
new file mode 100644
index 0000000..39e2cd3
--- /dev/null
+++ b/packages/nativescript-webview-ext/index.d.ts
@@ -0,0 +1,35 @@
+import { WebViewExtBase } from './common';
+export * from './common';
+export declare class WebViewExt extends WebViewExtBase {
+ ios: any /* WKWebView */;
+ static supportXLocalScheme: boolean;
+ readonly supportXLocalScheme: boolean;
+ viewPortSize: {
+ initialScale: number;
+ };
+ private limitsNavigationsToAppBoundDomains;
+ protected injectWebViewBridge(): Promise;
+ protected injectViewPortMeta(): Promise;
+ executeJavaScript(scriptCode: string, stringifyResult?: boolean): Promise;
+ onLoaded(): void;
+ onUnloaded(): void;
+ stopLoading(): void;
+ _loadUrl(src: string): void;
+ _loadData(content: string): void;
+ get canGoBack(): boolean;
+ get canGoForward(): boolean;
+ goBack(): void;
+ goForward(): void;
+ reload(): void;
+ _webAlert(message: string, callback: () => void): boolean;
+ _webConfirm(message: string, callback: (response: boolean | null) => void): boolean;
+ _webPrompt(message: string, defaultText: string, callback: (response: string | null) => void): boolean;
+ registerLocalResource(resourceName: string, path: string): void;
+ unregisterLocalResource(resourceName: string): void;
+ getRegisteredLocalResource(resourceName: string): string;
+ getTitle(): Promise;
+ autoLoadStyleSheetFile(resourceName: string, path: string, insertBefore?: boolean): Promise;
+ removeAutoLoadStyleSheetFile(resourceName: string): void;
+ autoLoadJavaScriptFile(resourceName: string, path: string): Promise;
+ removeAutoLoadJavaScriptFile(resourceName: string): void;
+}
diff --git a/packages/nativescript-webview-ext/index.ios.ts b/packages/nativescript-webview-ext/index.ios.ts
new file mode 100644
index 0000000..33411bb
--- /dev/null
+++ b/packages/nativescript-webview-ext/index.ios.ts
@@ -0,0 +1,780 @@
+import '@nativescript/core';
+import { alert, confirm, File, knownFolders, profile, prompt, Trace } from '@nativescript/core';
+import { isEnabledProperty } from '@nativescript/core/ui/core/view';
+import { webViewBridge } from './bridge-loader';
+import { autoInjectJSBridgeProperty, limitsNavigationsToAppBoundDomainsProperty, NavigationType, scrollBounceProperty, ViewPortProperties, viewPortProperty, WebViewExtBase } from './common';
+
+export * from './common';
+
+const messageHandlerName = 'nsBridge';
+
+export class WebViewExt extends WebViewExtBase {
+ public ios: WKWebView;
+
+ public static supportXLocalScheme = typeof CustomUrlSchemeHandler !== 'undefined';
+
+ protected wkWebViewConfiguration: WKWebViewConfiguration;
+ protected wkNavigationDelegate: WKNavigationDelegateNotaImpl;
+ protected wkUIDelegate: WKUIDelegateNotaImpl;
+ protected wkCustomUrlSchemeHandler: CustomUrlSchemeHandler | void;
+ protected wkUserContentController: WKUserContentController;
+ protected wkUserScriptInjectWebViewBridge?: WKUserScript;
+ protected wkUserScriptViewPortCode: Promise | null;
+ protected wkNamedUserScripts = [] as Array<{
+ resourceName: string;
+ wkUserScript: WKUserScript;
+ }>;
+
+ public readonly supportXLocalScheme = typeof CustomUrlSchemeHandler !== 'undefined';
+
+ public viewPortSize = { initialScale: 1.0 };
+ private limitsNavigationsToAppBoundDomains = false;
+
+ public createNativeView() {
+ const configuration = WKWebViewConfiguration.new();
+ configuration.dataDetectorTypes = WKDataDetectorTypes.All;
+ this.wkWebViewConfiguration = configuration;
+
+ const messageHandler = WKScriptMessageHandlerNotaImpl.initWithOwner(new WeakRef(this));
+ const wkUController = (this.wkUserContentController = WKUserContentController.new());
+ wkUController.addScriptMessageHandlerName(messageHandler, messageHandlerName);
+ configuration.userContentController = wkUController;
+ configuration.preferences.setValueForKey(true, 'allowFileAccessFromFileURLs');
+ configuration.setValueForKey(true, 'allowUniversalAccessFromFileURLs');
+ configuration.limitsNavigationsToAppBoundDomains = this.limitsNavigationsToAppBoundDomains;
+ if (this.supportXLocalScheme) {
+ this.wkCustomUrlSchemeHandler = new CustomUrlSchemeHandler();
+ configuration.setURLSchemeHandlerForURLScheme(this.wkCustomUrlSchemeHandler, this.interceptScheme);
+ }
+
+ const webview = new WKWebView({
+ frame: CGRectZero,
+ configuration,
+ });
+
+ return webview;
+ }
+
+ public initNativeView() {
+ super.initNativeView();
+
+ this.wkNavigationDelegate = WKNavigationDelegateNotaImpl.initWithOwner(new WeakRef(this));
+ this.wkUIDelegate = WKUIDelegateNotaImpl.initWithOwner(new WeakRef(this));
+
+ this.loadWKUserScripts();
+ }
+
+ public disposeNativeView() {
+ this.wkWebViewConfiguration?.userContentController?.removeScriptMessageHandlerForName(messageHandlerName);
+ this.wkWebViewConfiguration = null!;
+ this.wkNavigationDelegate = null!;
+ this.wkCustomUrlSchemeHandler = null!;
+ this.wkUIDelegate = null!;
+
+ super.disposeNativeView();
+ }
+
+ protected injectWebViewBridge() {
+ return this.ensurePolyfills();
+ }
+
+ protected async injectViewPortMeta() {
+ this.resetViewPortCode();
+ if (this.supportXLocalScheme) {
+ return;
+ }
+
+ return await super.injectViewPortMeta();
+ }
+
+ public async executeJavaScript(scriptCode: string, stringifyResult = true): Promise {
+ if (stringifyResult) {
+ scriptCode = `
+ (function(window) {
+ var result = null;
+
+ try {
+ result = ${scriptCode.trim()};
+ } catch (err) {
+ return JSON.stringify({
+ error: true,
+ message: err.message,
+ stack: err.stack
+ });
+ }
+
+ try {
+ return JSON.stringify(result);
+ } catch (err) {
+ return result;
+ }
+ })(window);
+ `;
+ }
+
+ const nativeView = this.nativeViewProtected;
+ if (!nativeView) {
+ return Promise.reject(new Error('WebView is missing'));
+ }
+
+ const rawResult = await new Promise((resolve, reject) => {
+ nativeView.evaluateJavaScriptCompletionHandler(scriptCode.trim(), (result, error) => {
+ if (error) {
+ reject(error);
+
+ return;
+ }
+
+ resolve(result);
+ });
+ });
+
+ const result: T = await this.parseWebViewJavascriptResult(rawResult);
+
+ const r = result as any;
+ if (r && typeof r === 'object' && r.error) {
+ const error = new Error(r.message);
+ (error as any).webStack = r.stack;
+ throw error;
+ }
+
+ return result;
+ }
+
+ @profile
+ public onLoaded() {
+ super.onLoaded();
+
+ const nativeView = this.nativeViewProtected;
+ if (nativeView) {
+ nativeView.navigationDelegate = this.wkNavigationDelegate;
+ nativeView.UIDelegate = this.wkUIDelegate;
+ }
+ }
+
+ public onUnloaded() {
+ const nativeView = this.nativeViewProtected;
+ if (nativeView) {
+ nativeView.navigationDelegate = null!;
+ nativeView.UIDelegate = null!;
+ }
+
+ super.onUnloaded();
+ }
+
+ public stopLoading() {
+ const nativeView = this.nativeViewProtected;
+ if (!nativeView) {
+ return;
+ }
+
+ nativeView.stopLoading();
+ }
+
+ public _loadUrl(src: string) {
+ const nativeView = this.nativeViewProtected;
+ if (!nativeView) {
+ return;
+ }
+
+ const nsURL = NSURL.URLWithString(src);
+ if (src.startsWith('file:///')) {
+ const cachePath = src.substring(0, src.lastIndexOf('/'));
+ const nsReadAccessUrl = NSURL.URLWithString(cachePath);
+ this.writeTrace(`WKWebViewWrapper.loadUrl("${src}") -> ios.loadFileURLAllowingReadAccessToURL("${nsURL}", "${nsReadAccessUrl}"`);
+ nativeView.loadFileURLAllowingReadAccessToURL(nsURL, nsReadAccessUrl);
+ } else {
+ const nsRequestWithUrl = NSURLRequest.requestWithURL(nsURL);
+ this.writeTrace(`WKWebViewWrapper.loadUrl("${src}") -> ios.loadRequest("${nsRequestWithUrl}"`);
+ nativeView.loadRequest(nsRequestWithUrl);
+ }
+ }
+
+ public _loadData(content: string) {
+ const nativeView = this.nativeViewProtected;
+ if (!nativeView) {
+ return;
+ }
+
+ const baseUrl = `file:///${knownFolders.currentApp().path}/`;
+ const nsBaseUrl = NSURL.URLWithString(baseUrl);
+
+ this.writeTrace(`WKWebViewWrapper.loadUrl(content) -> this.ios.loadHTMLStringBaseURL("${nsBaseUrl}")`);
+ nativeView.loadHTMLStringBaseURL(content, nsBaseUrl);
+ }
+
+ public get canGoBack(): boolean {
+ const nativeView = this.nativeViewProtected;
+
+ return nativeView && !!nativeView.canGoBack;
+ }
+
+ public get canGoForward(): boolean {
+ const nativeView = this.nativeViewProtected;
+
+ return nativeView && !!nativeView.canGoForward;
+ }
+
+ public goBack() {
+ const nativeView = this.nativeViewProtected;
+ if (!nativeView) {
+ return;
+ }
+
+ nativeView.goBack();
+ }
+
+ public goForward() {
+ const nativeView = this.nativeViewProtected;
+ if (!nativeView) {
+ return;
+ }
+
+ nativeView.goForward();
+ }
+
+ public reload() {
+ const nativeView = this.nativeViewProtected;
+ if (!nativeView) {
+ return;
+ }
+
+ nativeView.reload();
+ }
+
+ public _webAlert(message: string, callback: () => void) {
+ if (!super._webAlert(message, callback)) {
+ alert(message)
+ .then(() => callback())
+ .catch(() => callback());
+ }
+
+ return true;
+ }
+
+ public _webConfirm(message: string, callback: (response: boolean | null) => void) {
+ if (!super._webConfirm(message, callback)) {
+ confirm(message)
+ .then((res) => callback(res))
+ .catch(() => callback(null));
+ }
+
+ return true;
+ }
+
+ public _webPrompt(message: string, defaultText: string, callback: (response: string | null) => void) {
+ if (!super._webPrompt(message, defaultText, callback)) {
+ prompt(message, defaultText)
+ .then((res) => {
+ if (res.result) {
+ callback(res.text);
+ } else {
+ callback(null);
+ }
+ })
+ .catch(() => callback(null));
+ }
+
+ return true;
+ }
+
+ public registerLocalResource(resourceName: string, path: string) {
+ const cls = `WebViewExt<${this}.ios>.registerLocalResource("${resourceName}", "${path}")`;
+
+ if (!this.supportXLocalScheme) {
+ this.writeTrace(`${cls} -> custom schema isn't support on iOS <11`, Trace.messageType.error);
+
+ return;
+ }
+
+ resourceName = this.fixLocalResourceName(resourceName);
+
+ const filepath = this.resolveLocalResourceFilePath(path);
+ if (!filepath) {
+ this.writeTrace(`${cls} -> file doesn't exist`, Trace.messageType.error);
+
+ return;
+ }
+
+ this.writeTrace(`${cls} -> file: "${filepath}"`);
+
+ this.registerLocalResourceForNative(resourceName, filepath);
+ }
+
+ public unregisterLocalResource(resourceName: string) {
+ const cls = `WebViewExt<${this}.ios>.unregisterLocalResource("${resourceName}")`;
+ if (!this.supportXLocalScheme) {
+ this.writeTrace(`${cls} -> custom schema isn't support on iOS <11`, Trace.messageType.error);
+
+ return;
+ }
+
+ this.writeTrace(cls);
+
+ resourceName = this.fixLocalResourceName(resourceName);
+
+ this.unregisterLocalResourceForNative(resourceName);
+ }
+
+ public getRegisteredLocalResource(resourceName: string) {
+ resourceName = this.fixLocalResourceName(resourceName);
+ const cls = `WebViewExt<${this}.ios>.getRegisteredLocalResource("${resourceName}")`;
+ if (!this.supportXLocalScheme) {
+ this.writeTrace(`${cls} -> custom schema isn't support on iOS <11`, Trace.messageType.error);
+
+ return;
+ }
+
+ let result = this.getRegisteredLocalResourceFromNative(resourceName);
+
+ this.writeTrace(`${cls} -> "${result}"`);
+
+ return result;
+ }
+
+ public getTitle() {
+ return this.executeJavaScript('document.title');
+ }
+
+ public async autoLoadStyleSheetFile(resourceName: string, path: string, insertBefore?: boolean) {
+ const filepath = this.resolveLocalResourceFilePath(path);
+ if (!filepath) {
+ this.writeTrace(`WKWebViewWrapper.autoLoadStyleSheetFile("${resourceName}", "${path}") - couldn't resolve filepath`);
+
+ return;
+ }
+
+ resourceName = this.fixLocalResourceName(resourceName);
+ const scriptCode = await this.generateLoadCSSFileScriptCode(resourceName, filepath, insertBefore);
+
+ if (scriptCode) {
+ this.addNamedWKUserScript(`auto-load-css-${resourceName}`, scriptCode);
+ }
+ }
+
+ public removeAutoLoadStyleSheetFile(resourceName: string) {
+ resourceName = this.fixLocalResourceName(resourceName);
+ this.removeNamedWKUserScript(`auto-load-css-${resourceName}`);
+ }
+
+ public async autoLoadJavaScriptFile(resourceName: string, path: string) {
+ const filepath = this.resolveLocalResourceFilePath(path);
+ if (!filepath) {
+ this.writeTrace(`WKWebViewWrapper.autoLoadJavaScriptFile("${resourceName}", "${path}") - couldn't resolve filepath`);
+
+ return;
+ }
+
+ const scriptCode = await File.fromPath(filepath).readText();
+
+ this.addNamedWKUserScript(resourceName, scriptCode);
+ }
+
+ public removeAutoLoadJavaScriptFile(resourceName: string) {
+ const fixedResourceName = this.fixLocalResourceName(resourceName);
+ const href = `${this.interceptScheme}://${fixedResourceName}`;
+ this.removeNamedWKUserScript(href);
+ }
+
+ [autoInjectJSBridgeProperty.setNative](enabled: boolean) {
+ this.loadWKUserScripts(enabled);
+ }
+
+ [scrollBounceProperty.getDefault]() {
+ const nativeView = this.nativeViewProtected;
+ if (!nativeView) {
+ return false;
+ }
+ return nativeView.scrollView.bounces;
+ }
+
+ [scrollBounceProperty.setNative](enabled: boolean) {
+ const nativeView = this.nativeViewProtected;
+ if (!nativeView) {
+ return;
+ }
+
+ nativeView.scrollView.bounces = !!enabled;
+ }
+
+ [viewPortProperty.setNative](value: ViewPortProperties) {
+ if (this.src) {
+ this.injectViewPortMeta();
+ }
+ }
+
+ [limitsNavigationsToAppBoundDomainsProperty.setNative](enabled: boolean) {
+ this.limitsNavigationsToAppBoundDomains = enabled;
+ }
+
+ [limitsNavigationsToAppBoundDomainsProperty.getDefault]() {
+ return false;
+ }
+
+ [isEnabledProperty.setNative](enabled: boolean) {
+ const nativeView = this.nativeViewProtected;
+ if (!nativeView) {
+ return;
+ }
+
+ nativeView.userInteractionEnabled = !!enabled;
+ nativeView.scrollView.userInteractionEnabled = !!enabled;
+ }
+
+ /**
+ * iOS11+
+ *
+ * Sets up loading WKUserScripts
+ *
+ * @param autoInjectJSBridge If true viewport-code, bridge-code and named scripts will be loaded, if false only viewport-code
+ */
+ protected loadWKUserScripts(autoInjectJSBridge = this.autoInjectJSBridge) {
+ if (!this.wkUserScriptViewPortCode) {
+ this.wkUserScriptViewPortCode = this.makeWKUserScriptPromise(this.generateViewPortCode());
+ }
+
+ this.wkUserContentController.removeAllUserScripts();
+
+ this.addUserScriptFromPromise(this.wkUserScriptViewPortCode!);
+ if (!autoInjectJSBridge) {
+ return;
+ }
+
+ if (!this.wkUserScriptInjectWebViewBridge) {
+ this.wkUserScriptInjectWebViewBridge = this.createWkUserScript(webViewBridge);
+ }
+
+ this.addUserScript(this.wkUserScriptInjectWebViewBridge);
+ for (const { wkUserScript } of this.wkNamedUserScripts) {
+ this.addUserScript(wkUserScript);
+ }
+ }
+
+ /**
+ * iOS11+
+ *
+ * Remove a named WKUserScript
+ */
+ protected removeNamedWKUserScript(resourceName: string) {
+ const idx = this.wkNamedUserScripts.findIndex((val) => val.resourceName === resourceName);
+ if (idx === -1) {
+ return;
+ }
+
+ this.wkNamedUserScripts.splice(idx, 1);
+
+ this.loadWKUserScripts();
+ }
+
+ protected async resetViewPortCode() {
+ this.wkUserScriptViewPortCode = null;
+
+ const viewPortScriptCode = await this.generateViewPortCode();
+ if (viewPortScriptCode) {
+ this.executeJavaScript(viewPortScriptCode);
+ this.loadWKUserScripts();
+ }
+ }
+
+ protected registerLocalResourceForNative(resourceName: string, filepath: string) {
+ if (!this.wkCustomUrlSchemeHandler) {
+ return;
+ }
+
+ this.wkCustomUrlSchemeHandler.registerLocalResourceForKeyFilepath(resourceName, filepath);
+ }
+
+ protected unregisterLocalResourceForNative(resourceName: string) {
+ if (!this.wkCustomUrlSchemeHandler) {
+ return;
+ }
+
+ this.wkCustomUrlSchemeHandler.unregisterLocalResourceForKey(resourceName);
+ }
+
+ protected getRegisteredLocalResourceFromNative(resourceName: string) {
+ if (!this.wkCustomUrlSchemeHandler) {
+ return;
+ }
+
+ return this.wkCustomUrlSchemeHandler.getRegisteredLocalResourceForKey(resourceName);
+ }
+
+ protected async makeWKUserScriptPromise(scriptCodePromise: Promise): Promise {
+ const scriptCode = await scriptCodePromise;
+ if (!scriptCode) {
+ return null;
+ }
+
+ return this.createWkUserScript(scriptCode);
+ }
+
+ protected async addUserScriptFromPromise(userScriptPromise: Promise) {
+ const userScript = await userScriptPromise;
+ if (!userScript) {
+ return;
+ }
+
+ return this.addUserScript(userScript);
+ }
+
+ protected addUserScript(userScript: WKUserScript | null) {
+ if (!userScript) {
+ return;
+ }
+
+ this.wkUserContentController.addUserScript(userScript);
+ }
+
+ /**
+ * iOS11+
+ *
+ * Add/replace a named WKUserScript.
+ * These scripts will be injected when a new document is loaded.
+ */
+ protected addNamedWKUserScript(resourceName: string, scriptCode: string) {
+ if (!scriptCode) {
+ return;
+ }
+
+ this.removeNamedWKUserScript(resourceName);
+
+ const wkUserScript = this.createWkUserScript(scriptCode);
+
+ this.wkNamedUserScripts.push({ resourceName, wkUserScript });
+
+ this.addUserScript(wkUserScript);
+ }
+
+ /**
+ * iOS11+
+ *
+ * Factory function for creating a WKUserScript instance.
+ */
+ protected createWkUserScript(source: string) {
+ return WKUserScript.alloc().initWithSourceInjectionTimeForMainFrameOnly(source, WKUserScriptInjectionTime.AtDocumentEnd, true);
+ }
+}
+
+@NativeClass()
+export class WKNavigationDelegateNotaImpl extends NSObject implements WKNavigationDelegate {
+ public static ObjCProtocols = [WKNavigationDelegate];
+ public static initWithOwner(owner: WeakRef): WKNavigationDelegateNotaImpl {
+ const handler = WKNavigationDelegateNotaImpl.new();
+ handler.owner = owner;
+
+ return handler;
+ }
+
+ private owner: WeakRef;
+
+ public webViewDecidePolicyForNavigationActionDecisionHandler(webView: WKWebView, navigationAction: WKNavigationAction, decisionHandler: (policy: WKNavigationActionPolicy) => void): void {
+ const owner = this.owner.get();
+ if (!owner) {
+ decisionHandler(WKNavigationActionPolicy.Cancel);
+
+ return;
+ }
+
+ const request = navigationAction.request;
+ const httpMethod = request.HTTPMethod;
+ const url = request.URL && request.URL.absoluteString;
+
+ owner.writeTrace(`webViewDecidePolicyForNavigationActionDecisionHandler: "${url}"`);
+ if (!url) {
+ return;
+ }
+
+ let navType: NavigationType = 'other';
+
+ switch (navigationAction.navigationType) {
+ case WKNavigationType.LinkActivated: {
+ navType = 'linkClicked';
+ break;
+ }
+ case WKNavigationType.FormSubmitted: {
+ navType = 'formSubmitted';
+ break;
+ }
+ case WKNavigationType.BackForward: {
+ navType = 'backForward';
+ break;
+ }
+ case WKNavigationType.Reload: {
+ navType = 'reload';
+ break;
+ }
+ case WKNavigationType.FormResubmitted: {
+ navType = 'formResubmitted';
+ break;
+ }
+ default: {
+ navType = 'other';
+ break;
+ }
+ }
+
+ const shouldOverrideUrlLoading = owner._onShouldOverrideUrlLoading(url, httpMethod, navType);
+ if (shouldOverrideUrlLoading === true) {
+ owner.writeTrace(`WKNavigationDelegateClass.webViewDecidePolicyForNavigationActionDecisionHandler("${url}", "${navigationAction.navigationType}") -> method:${httpMethod} "${navType}" -> cancel`);
+ decisionHandler(WKNavigationActionPolicy.Cancel);
+
+ return;
+ }
+ decisionHandler(WKNavigationActionPolicy.Allow);
+
+ owner.writeTrace(`WKNavigationDelegateClass.webViewDecidePolicyForNavigationActionDecisionHandler("${url}", "${navigationAction.navigationType}") -> method:${httpMethod} "${navType}"`);
+ owner._onLoadStarted(url, navType);
+ }
+
+ public webViewDidStartProvisionalNavigation(webView: WKWebView, navigation: WKNavigation): void {
+ const owner = this.owner.get();
+ if (!owner) {
+ return;
+ }
+
+ owner.writeTrace(`WKNavigationDelegateClass.webViewDidStartProvisionalNavigation("${webView.URL}")`);
+ }
+
+ public webViewDidFinishNavigation(webView: WKWebView, navigation: WKNavigation): void {
+ const owner = this.owner.get();
+ if (!owner) {
+ return;
+ }
+
+ owner.writeTrace(`WKNavigationDelegateClass.webViewDidFinishNavigation("${webView.URL}")`);
+ let src = owner.src;
+ if (webView.URL) {
+ src = webView.URL.absoluteString;
+ }
+ owner._onLoadFinished(src).catch(() => void 0);
+ }
+
+ public webViewDidFailNavigationWithError(webView: WKWebView, navigation: WKNavigation, error: NSError): void {
+ const owner = this.owner.get();
+ if (!owner) {
+ return;
+ }
+
+ let src = owner.src;
+ if (webView.URL) {
+ src = webView.URL.absoluteString;
+ }
+ owner.writeTrace(`WKNavigationDelegateClass.webViewDidFailNavigationWithError("${error.localizedDescription}")`);
+ owner._onLoadFinished(src, error.localizedDescription).catch(() => void 0);
+ }
+
+ public webViewDidFailProvisionalNavigationWithError(webView: WKWebView, navigation: WKNavigation, error: NSError): void {
+ const owner = this.owner.get();
+ if (!owner) {
+ return;
+ }
+
+ let src = owner.src;
+ if (webView.URL && webView.URL.absoluteString) {
+ src = webView.URL.absoluteString;
+ }
+
+ owner.writeTrace(`WKNavigationDelegateClass.webViewDidFailProvisionalNavigationWithError(${error.localizedDescription}`);
+ owner._onLoadFinished(src, error.localizedDescription).catch(() => void 0);
+ }
+}
+
+@NativeClass()
+export class WKScriptMessageHandlerNotaImpl extends NSObject implements WKScriptMessageHandler {
+ public static ObjCProtocols = [WKScriptMessageHandler];
+
+ private owner: WeakRef;
+
+ public static initWithOwner(owner: WeakRef): WKScriptMessageHandlerNotaImpl {
+ const delegate = WKScriptMessageHandlerNotaImpl.new();
+ delegate.owner = owner;
+
+ return delegate;
+ }
+
+ public userContentControllerDidReceiveScriptMessage(userContentController: WKUserContentController, webViewMessage: WKScriptMessage) {
+ const owner = this.owner.get();
+ if (!owner) {
+ return;
+ }
+
+ try {
+ const message = JSON.parse(webViewMessage.body as string);
+ owner.onWebViewEvent(message.eventName, message.data);
+ } catch (err) {
+ owner.writeTrace(`userContentControllerDidReceiveScriptMessage(${userContentController}, ${webViewMessage}) - bad message: ${JSON.stringify(webViewMessage.body)}`, Trace.messageType.error);
+ }
+ }
+}
+
+@NativeClass()
+export class WKUIDelegateNotaImpl extends NSObject implements WKUIDelegate {
+ public static ObjCProtocols = [WKUIDelegate];
+ public owner: WeakRef;
+
+ public static initWithOwner(owner: WeakRef): WKUIDelegateNotaImpl {
+ const delegate = WKUIDelegateNotaImpl.new();
+ delegate.owner = owner;
+ console.log(delegate);
+
+ return delegate;
+ }
+
+ /**
+ * Handle alerts from the webview
+ */
+ public webViewRunJavaScriptAlertPanelWithMessageInitiatedByFrameCompletionHandler(webView: WKWebView, message: string, frame: WKFrameInfo, completionHandler: () => void): void {
+ const owner = this.owner.get();
+ if (!owner) {
+ return;
+ }
+
+ let gotResponse = false;
+ owner._webAlert(message, () => {
+ if (!gotResponse) {
+ completionHandler();
+ }
+
+ gotResponse = true;
+ });
+ }
+
+ /**
+ * Handle confirm dialogs from the webview
+ */
+ public webViewRunJavaScriptConfirmPanelWithMessageInitiatedByFrameCompletionHandler(webView: WKWebView, message: string, frame: WKFrameInfo, completionHandler: (confirmed: boolean) => void): void {
+ const owner = this.owner.get();
+ if (!owner) {
+ return;
+ }
+
+ let gotResponse = false;
+ owner._webConfirm(message, (confirmed: boolean) => {
+ if (!gotResponse) {
+ completionHandler(confirmed);
+ }
+
+ gotResponse = true;
+ });
+ }
+
+ /**
+ * Handle prompt dialogs from the webview
+ */
+ public webViewRunJavaScriptTextInputPanelWithPromptDefaultTextInitiatedByFrameCompletionHandler(webView: WKWebView, message: string, defaultText: string, frame: WKFrameInfo, completionHandler: (response: string) => void): void {
+ const owner = this.owner.get();
+ if (!owner) {
+ return;
+ }
+
+ let gotResponse = false;
+ owner._webPrompt(message, defaultText, (response: string) => {
+ if (!gotResponse) {
+ completionHandler(response);
+ }
+
+ gotResponse = true;
+ });
+ }
+}
diff --git a/packages/nativescript-webview-ext/package.json b/packages/nativescript-webview-ext/package.json
new file mode 100644
index 0000000..4c83f9f
--- /dev/null
+++ b/packages/nativescript-webview-ext/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@essent/nativescript-webview-ext",
+ "version": "9.0.1",
+ "description": "Extended WebView for NativeScript which adds 'x-local' scheme for local-files. events between WebView and native-layer, javascript execution, injecting CSS and JS-files.",
+ "main": "index",
+ "typings": "index.d.ts",
+ "nativescript": {
+ "platforms": {
+ "ios": "6.0.0",
+ "android": "6.0.0"
+ }
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/Essent/nativescript-plugins.git"
+ },
+ "keywords": [
+ "NativeScript",
+ "JavaScript",
+ "TypeScript",
+ "iOS",
+ "Android"
+ ],
+ "author": {
+ "name": "Essent",
+ "email": "frontend-licenties@essent.nl"
+ },
+ "bugs": {
+ "url": "https://github.com/Essent/nativescript-plugins/issues"
+ },
+ "license": "Apache-2.0",
+ "homepage": "https://github.com/Essent/nativescript-plugins",
+ "readmeFilename": "README.md",
+ "bootstrapper": "@nativescript/plugin-seed"
+}
diff --git a/packages/nativescript-webview-ext/project.json b/packages/nativescript-webview-ext/project.json
new file mode 100644
index 0000000..92373d5
--- /dev/null
+++ b/packages/nativescript-webview-ext/project.json
@@ -0,0 +1,64 @@
+{
+ "name": "nativescript-webview-ext",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "library",
+ "sourceRoot": "packages/nativescript-webview-ext",
+ "targets": {
+ "build": {
+ "executor": "@nx/js:tsc",
+ "options": {
+ "outputPath": "dist/packages/nativescript-webview-ext",
+ "tsConfig": "packages/nativescript-webview-ext/tsconfig.json",
+ "packageJson": "packages/nativescript-webview-ext/package.json",
+ "main": "packages/nativescript-webview-ext/index.d.ts",
+ "assets": [
+ "packages/nativescript-webview-ext/*.md",
+ "packages/nativescript-webview-ext/index.d.ts",
+ "LICENSE",
+ {
+ "glob": "**/*",
+ "input": "packages/nativescript-webview-ext/platforms/",
+ "output": "./platforms/"
+ }
+ ],
+ "dependsOn": [
+ {
+ "target": "build.all",
+ "projects": "dependencies"
+ }
+ ]
+ }
+ },
+ "build.all": {
+ "executor": "nx:run-commands",
+ "options": {
+ "commands": ["cd tools/scripts/www-src && npm run build && cd ..", "cd tools/scripts && npx ts-node ./bridge-loader-make.ts", "node tools/scripts/build-finish.ts nativescript-webview-ext"],
+ "parallel": false
+ },
+ "outputs": ["dist/packages/nativescript-webview-ext"],
+ "dependsOn": [
+ {
+ "target": "build.all",
+ "dependencies": true
+ },
+ {
+ "target": "build"
+ }
+ ]
+ },
+ "focus": {
+ "executor": "nx:run-commands",
+ "options": {
+ "commands": ["nx g @nativescript/plugin-tools:focus-packages nativescript-webview-ext"],
+ "parallel": false
+ }
+ },
+ "lint": {
+ "executor": "@nx/linter:eslint",
+ "options": {
+ "lintFilePatterns": ["packages/nativescript-webview-ext/**/*.ts"]
+ }
+ }
+ },
+ "tags": []
+}
diff --git a/packages/nativescript-webview-ext/references.d.ts b/packages/nativescript-webview-ext/references.d.ts
new file mode 100644
index 0000000..22bac92
--- /dev/null
+++ b/packages/nativescript-webview-ext/references.d.ts
@@ -0,0 +1 @@
+///
diff --git a/packages/nativescript-webview-ext/tsconfig.json b/packages/nativescript-webview-ext/tsconfig.json
new file mode 100644
index 0000000..aed7323
--- /dev/null
+++ b/packages/nativescript-webview-ext/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "rootDir": "."
+ },
+ "exclude": ["**/*.spec.ts", "**/*.test.ts", "angular"],
+ "include": ["**/*.ts", "references.d.ts"]
+}
diff --git a/packages/nativescript-webview-ext/typings/NotaWebViewExt.d.ts b/packages/nativescript-webview-ext/typings/NotaWebViewExt.d.ts
new file mode 100644
index 0000000..7730178
--- /dev/null
+++ b/packages/nativescript-webview-ext/typings/NotaWebViewExt.d.ts
@@ -0,0 +1,29 @@
+///
+
+declare class CustomUrlSchemeHandler extends NSObject {
+ static alloc(): CustomUrlSchemeHandler; // inherited from NSObject
+
+ static new(): CustomUrlSchemeHandler; // inherited from NSObject
+
+ checkTcpPortForListenWithPort(port: number): boolean;
+
+ clearRegisteredLocalResource(): void;
+
+ getRegisteredLocalResourceForKey(forKey: string): string;
+
+ registerLocalResourceForKeyFilepath(forKey: string, filepath: string): void;
+
+ resolveFilePath(url: NSURL): string;
+
+ resolveMimeTypeFromFilepath(filepath: string): string;
+
+ unregisterLocalResourceForKey(forKey: string): void;
+
+ webViewStartURLSchemeTask(webView: WKWebView, urlSchemeTask: WKURLSchemeTask): void;
+
+ webViewStopURLSchemeTask(webView: WKWebView, urlSchemeTask: WKURLSchemeTask): void;
+}
+
+declare var NotaWebViewExtVersionNumber: number;
+
+declare var NotaWebViewExtVersionString: interop.Reference;
diff --git a/packages/nativescript-webview-ext/typings/webviewinterface.d.ts b/packages/nativescript-webview-ext/typings/webviewinterface.d.ts
new file mode 100644
index 0000000..5504be8
--- /dev/null
+++ b/packages/nativescript-webview-ext/typings/webviewinterface.d.ts
@@ -0,0 +1,13 @@
+///
+
+declare namespace dk {
+ export namespace nota {
+ export namespace webviewinterface {
+ export class WebViewBridgeInterface {
+ public emitEvent(param0: string, param1: string): void;
+ public emitEventToNativeScript(param0: string, param1: string): void;
+ public constructor();
+ }
+ }
+ }
+}
diff --git a/tools/demo/index.ts b/tools/demo/index.ts
index a0962ff..920ee07 100644
--- a/tools/demo/index.ts
+++ b/tools/demo/index.ts
@@ -5,3 +5,4 @@ export * from './nativescript-iadvize';
export * from './nativescript-medallia';
export * from './nativescript-ng-sentry';
export * from './nativescript-urban-airship';
+export * from './nativescript-webview-ext';
diff --git a/tools/demo/nativescript-webview-ext/index.ts b/tools/demo/nativescript-webview-ext/index.ts
new file mode 100644
index 0000000..74c2b37
--- /dev/null
+++ b/tools/demo/nativescript-webview-ext/index.ts
@@ -0,0 +1,8 @@
+import { DemoSharedBase } from '../utils';
+import {} from '@essent/nativescript-webview-ext';
+
+export class DemoSharedNativescriptWebviewExt extends DemoSharedBase {
+ testIt() {
+ console.log('test nativescript-webview-ext!');
+ }
+}
diff --git a/tools/scripts/bridge-loader-make.ts b/tools/scripts/bridge-loader-make.ts
new file mode 100644
index 0000000..2f9cde8
--- /dev/null
+++ b/tools/scripts/bridge-loader-make.ts
@@ -0,0 +1,35 @@
+require('tslib');
+
+const fs = require('fs');
+const Terser = require('terser');
+const { promisify } = require('util');
+
+const fsWriteFile = promisify(fs.writeFile);
+const fsReadFile = promisify(fs.readFile);
+
+async function nativescriptWebviewBridgeLoader() {
+ let template = await fsReadFile('./bridge-loader.ts.tmpl', 'UTF-8');
+
+ const values = {
+ fetchPolyfill: await fsReadFile('../../node_modules/whatwg-fetch/dist/fetch.umd.js', 'UTF-8'),
+ promisePolyfill: await fsReadFile('../../node_modules/promise-polyfill/dist/polyfill.js', 'UTF-8'),
+ webViewBridge: await fsReadFile('./www/ns-webview-bridge.js', 'UTF-8'),
+ metadataViewPort: await fsReadFile('./www/metadata-view-port.js', 'UTF-8'),
+ };
+
+ for (const [name, value] of Object.entries(values)) {
+ const terserRes = await Terser.minify(value, {
+ compress: true,
+ mangle: false,
+ });
+ template = template.replace(`= ${name} ?>`, JSON.stringify(terserRes.code));
+ }
+
+ await fsWriteFile('../../packages/nativescript-webview-ext/bridge-loader.ts', template);
+}
+
+async function worker() {
+ await nativescriptWebviewBridgeLoader();
+}
+
+worker();
diff --git a/tools/scripts/bridge-loader.ts.tmpl b/tools/scripts/bridge-loader.ts.tmpl
new file mode 100644
index 0000000..8e842f9
--- /dev/null
+++ b/tools/scripts/bridge-loader.ts.tmpl
@@ -0,0 +1,4 @@
+export const fetchPolyfill: string = = fetchPolyfill ?>;
+export const promisePolyfill: string = = promisePolyfill ?>;
+export const webViewBridge: string = = webViewBridge ?>;
+export const metadataViewPort: string = = metadataViewPort ?>;
diff --git a/tools/scripts/build-finish.ts b/tools/scripts/build-finish.ts
index 9b0c063..66014d1 100644
--- a/tools/scripts/build-finish.ts
+++ b/tools/scripts/build-finish.ts
@@ -1,7 +1,7 @@
const ngPackage = require('ng-packagr');
const path = require('path');
const fs = require('fs-extra');
-const devkit = require('@nrwl/devkit');
+const devkit = require('@nx/devkit');
const rootDir = path.resolve(path.join(__dirname, '..', '..'));
const nxConfigPath = path.resolve(path.join(rootDir, 'nx.json'));
diff --git a/tools/scripts/www-src/.prettierignore b/tools/scripts/www-src/.prettierignore
new file mode 100644
index 0000000..6cfef1c
--- /dev/null
+++ b/tools/scripts/www-src/.prettierignore
@@ -0,0 +1,3 @@
+**/*.d.ts
+**/node_modules
+**/platforms
diff --git a/tools/scripts/www-src/LICENSE b/tools/scripts/www-src/LICENSE
new file mode 100644
index 0000000..a36a170
--- /dev/null
+++ b/tools/scripts/www-src/LICENSE
@@ -0,0 +1,20 @@
+
+The MIT License (MIT)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/tools/scripts/www-src/metadata-view-port.ts b/tools/scripts/www-src/metadata-view-port.ts
new file mode 100644
index 0000000..8afc7d5
--- /dev/null
+++ b/tools/scripts/www-src/metadata-view-port.ts
@@ -0,0 +1,64 @@
+interface ViewPortProperties {
+ width?: number | 'device-width';
+ height?: number | 'device-height';
+ initialScale?: number;
+ maximumScale?: number;
+ minimumScale?: number;
+ userScalable?: boolean | 'yes' | 'no';
+}
+
+(function (window) {
+ const defaultViewPort: ViewPortProperties = {
+ initialScale: 1.0,
+ };
+
+ const document = window.document;
+ let meta: HTMLMetaElement | null = document.querySelector('head meta[name="viewport"]');
+ if (!meta) {
+ meta = document.createElement('meta');
+ meta.setAttribute('name', 'viewport');
+
+ document.head.appendChild(meta);
+ }
+
+ const viewPortInput = '<%= VIEW_PORT %>';
+ let viewPortValues = defaultViewPort;
+ if (viewPortInput && typeof viewPortInput === 'object') {
+ viewPortValues = viewPortInput;
+ }
+
+ const { initialScale = defaultViewPort.initialScale, width, height, userScalable, minimumScale, maximumScale } = viewPortValues;
+
+ const content = [`initial-scale=${initialScale}`] as string[];
+
+ if (width) {
+ content.push(`width=${width}`);
+ }
+
+ if (height) {
+ content.push(`height=${height}`);
+ }
+
+ if (typeof userScalable === 'boolean') {
+ content.push(`user-scalable=${userScalable ? 'yes' : 'no'}`);
+ } else if (typeof userScalable === 'string') {
+ const lcUserScalable = `${userScalable}`.toLowerCase();
+ if (lcUserScalable === 'yes') {
+ content.push(`user-scalable=yes`);
+ } else if (lcUserScalable === 'no') {
+ content.push(`user-scalable=no`);
+ } else {
+ console.error(`userScalable=${JSON.stringify(userScalable)} is an unknown value`);
+ }
+ }
+
+ if (minimumScale) {
+ content.push(`minimum-scale=${minimumScale}`);
+ }
+
+ if (maximumScale) {
+ content.push(`maximum-scale=${maximumScale}`);
+ }
+
+ meta.setAttribute('content', content.join(', '));
+})(window);
diff --git a/tools/scripts/www-src/ns-webview-bridge.ts b/tools/scripts/www-src/ns-webview-bridge.ts
new file mode 100644
index 0000000..9e44617
--- /dev/null
+++ b/tools/scripts/www-src/ns-webview-bridge.ts
@@ -0,0 +1,449 @@
+interface EventListener {
+ (data: any): void | boolean;
+}
+
+interface EventListenerMap {
+ [eventName: string]: EventListener[];
+}
+
+declare const androidWebViewBridge: {
+ emitEvent(eventName: string, data: string): void;
+};
+
+interface WKWebViewMessageHandler {
+ postMessage(message: string): void;
+}
+
+if (!Object.keys) {
+ Object.keys = (function () {
+ 'use strict';
+ const hasOwnProperty = Object.prototype.hasOwnProperty;
+ const hasDontEnumBug = !{ toString: null }.propertyIsEnumerable('toString');
+ const dontEnums = ['toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'constructor'];
+ const dontEnumsLength = dontEnums.length;
+
+ return function (obj) {
+ if (typeof obj !== 'function' && (typeof obj !== 'object' || obj === null)) {
+ throw new TypeError('Object.keys called on non-object');
+ }
+
+ const result = new Array();
+
+ for (const prop in obj) {
+ if (hasOwnProperty.call(obj, prop)) {
+ result.push(prop);
+ }
+ }
+
+ if (hasDontEnumBug) {
+ for (let i = 0; i < dontEnumsLength; i++) {
+ if (hasOwnProperty.call(obj, dontEnums[i])) {
+ result.push(dontEnums[i]);
+ }
+ }
+ }
+
+ return result;
+ };
+ })();
+}
+
+if (!Object.entries) {
+ Object.entries = function (this: null, obj: any) {
+ const ownProps = Object.keys(obj);
+ let i = ownProps.length;
+ const resArray = new Array(i); // preallocate the Array
+
+ while (i--) {
+ resArray[i] = [ownProps[i], obj[ownProps[i]]];
+ }
+
+ return resArray;
+ };
+}
+
+/**
+ * With WKWebView it's assumed the there is a WKScriptMessage named nsBridge
+ */
+function getWkWebViewMessageHandler(): WKWebViewMessageHandler | void {
+ const w = window as any;
+ if (!w?.webkit?.messageHandlers?.nsBridge) {
+ console.error(`Cannot get the window.webkit.messageHandlers.nsBridge - we can't communicate with native-layer`);
+
+ return;
+ }
+
+ return w.webkit.messageHandlers.nsBridge;
+}
+
+// Forked from nativescript-webview-interface@1.4.2
+class NSWebViewBridge {
+ /**
+ * Mapping of native eventName and its handler in webView
+ */
+ private eventListenerMap: EventListenerMap = {};
+
+ private get androidWebViewBridge() {
+ if (typeof androidWebViewBridge !== 'undefined') {
+ return androidWebViewBridge;
+ }
+ }
+
+ /**
+ * Handles events/commands emitted by android/ios. This function is called from nativescript.
+ */
+ public onNativeEvent(eventName: string, data: any) {
+ const events = this.eventListenerMap[eventName];
+ if (!events?.length) {
+ return;
+ }
+
+ for (const listener of events) {
+ const res = listener?.(data);
+ // if any handler return false, not executing any further handlers for that event.
+ if (res === false) {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Emit event to native layer on iOS.
+ *
+ * With WKWebView it's assumed the there is a WKScriptMessage named nsBridge
+ */
+ private emitEventToIOS(eventName: string, data: any) {
+ const messageHandler = getWkWebViewMessageHandler();
+ if (messageHandler) {
+ messageHandler.postMessage(
+ JSON.stringify({
+ eventName,
+ data,
+ })
+ );
+
+ return;
+ }
+
+ console.error('NSWebViewBridge only supports WKWebView');
+ }
+
+ /**
+ * Calls native android function to emit event and payload to android
+ */
+ private emitEventToAndroid(eventName: any, data: any) {
+ const androidWebViewBridge = this.androidWebViewBridge;
+ if (!androidWebViewBridge) {
+ console.error(`Tried to emit to android without the androidWebViewBridge`);
+
+ return;
+ }
+
+ androidWebViewBridge.emitEvent(eventName, data ?? 'null');
+ }
+
+ /**
+ * Add an event listener for event from native-layer
+ */
+ public on(eventName: string, callback: EventListener) {
+ if (!callback) {
+ return;
+ }
+
+ if (!this.eventListenerMap[eventName]) {
+ this.eventListenerMap[eventName] = [];
+ }
+
+ this.eventListenerMap[eventName].push(callback);
+ }
+
+ /**
+ * Add an event listener for event from native-layer
+ */
+ public addEventListener(eventName: string, callback: EventListener) {
+ this.on(eventName, callback);
+ }
+
+ /**
+ * Remove an event listener for event from native-layer.
+ * If callback is undefined all events for the eventName will be removed.
+ */
+ public off(eventName?: string, callback?: EventListener) {
+ if (!eventName) {
+ this.eventListenerMap = {};
+
+ return;
+ }
+
+ if (!this.eventListenerMap[eventName]) {
+ return;
+ }
+
+ if (!callback) {
+ delete this.eventListenerMap[eventName];
+
+ return;
+ }
+
+ this.eventListenerMap[eventName] = this.eventListenerMap[eventName].filter((oldCallback) => oldCallback !== callback);
+
+ if (this.eventListenerMap[eventName].length === 0) {
+ delete this.eventListenerMap[eventName];
+ }
+ }
+
+ /**
+ * Remove an event listener for event from native-layer.
+ * If callback is undefined all events for the eventName will be removed.
+ */
+ public removeEventListener(eventName?: string, callback?: EventListener) {
+ return this.off(eventName, callback);
+ }
+
+ /**
+ * Emit an event to the native-layer
+ */
+ public emit(eventName: string, data: any) {
+ if (this.androidWebViewBridge) {
+ this.emitEventToAndroid(eventName, JSON.stringify(data));
+ } else {
+ this.emitEventToIOS(eventName, data);
+ }
+ }
+
+ /**
+ * Injects a javascript file.
+ * This is usually called from WebViewExt.loadJavaScriptFiles(...)
+ */
+ public injectJavaScriptFile(href: string): Promise {
+ const elId = this.elementIdFromHref(href);
+
+ if (document.getElementById(elId)) {
+ console.log(`${elId} already exists`);
+
+ return Promise.resolve();
+ }
+
+ return new Promise((resolve, reject) => {
+ const scriptElement = document.createElement('script');
+ scriptElement.async = true;
+ scriptElement.setAttribute('id', elId);
+ scriptElement.addEventListener('error', (error) => {
+ console.error(`Failed to load ${href} - error: ${error}`);
+ reject(this.serializeError(error));
+
+ if (scriptElement.parentElement) {
+ scriptElement.parentElement.removeChild(scriptElement);
+ }
+ });
+ scriptElement.addEventListener('load', function () {
+ console.info(`Loaded ${href}`);
+ window.requestAnimationFrame(() => {
+ resolve();
+ });
+
+ if (scriptElement.parentElement) {
+ scriptElement.parentElement.removeChild(scriptElement);
+ }
+ });
+ scriptElement.src = href;
+
+ document.body.appendChild(scriptElement);
+ });
+ }
+
+ /**
+ * Used to inject javascript-files on iOS<11 where we cannot support x-local
+ */
+ public injectJavaScript(elId: string, scriptCode: string): Promise {
+ if (document.getElementById(elId)) {
+ console.log(`${elId} already exists`);
+
+ return Promise.resolve();
+ }
+
+ return new Promise((resolve, reject) => {
+ const scriptElement = document.createElement('script');
+ scriptElement.setAttribute('id', elId);
+ scriptElement.addEventListener('error', function (error) {
+ console.error(`Failed to inject javascript- error: ${error}`);
+ reject(error);
+
+ if (scriptElement.parentElement) {
+ scriptElement.parentElement.removeChild(scriptElement);
+ }
+ });
+ scriptElement.text = scriptCode;
+
+ document.body.appendChild(scriptElement);
+
+ window.requestAnimationFrame(() => resolve());
+ });
+ }
+
+ /**
+ * Injects a StyleSheet file.
+ * This is usually called from WebViewExt.loadStyleSheetFiles(...)
+ */
+ public injectStyleSheetFile(href: string, insertBefore?: boolean): Promise {
+ const elId = this.elementIdFromHref(href);
+
+ if (document.getElementById(elId)) {
+ console.log(`${elId} already exists`);
+
+ return Promise.resolve();
+ }
+
+ return new Promise((resolve, reject) => {
+ const linkElement = document.createElement('link');
+ linkElement.addEventListener('error', (error) => {
+ console.error(`Failed to load ${href} - error: ${error}`);
+ reject(error);
+
+ if (linkElement.parentElement) {
+ linkElement.parentElement.removeChild(linkElement);
+ }
+ });
+ linkElement.addEventListener('load', () => {
+ console.info(`Loaded ${href}`);
+ window.requestAnimationFrame(() => {
+ resolve();
+ });
+ });
+ linkElement.setAttribute('id', elId);
+ linkElement.setAttribute('rel', 'stylesheet');
+ linkElement.setAttribute('type', 'text/css');
+ linkElement.setAttribute('href', href);
+ if (document.head) {
+ if (insertBefore && document.head.childElementCount > 0) {
+ document.head.insertBefore(linkElement, document.head.firstElementChild);
+ } else {
+ document.head.appendChild(linkElement);
+ }
+ }
+ });
+ }
+
+ /**
+ * Inject stylesheets into the page without using x-local. This is needed for iOS<11
+ */
+ public injectStyleSheet(elId: string, stylesheet: string, insertBefore?: boolean): Promise {
+ if (document.getElementById(elId)) {
+ console.log(`${elId} already exists`);
+
+ return Promise.resolve();
+ }
+
+ return new Promise((resolve, reject) => {
+ const styleElement = document.createElement('style');
+ styleElement.addEventListener('error', reject);
+ styleElement.textContent = stylesheet;
+ styleElement.setAttribute('id', elId);
+
+ const parentElement = document.head ?? document.body;
+ if (parentElement) {
+ if (insertBefore && parentElement.childElementCount > 0) {
+ document.head.insertBefore(styleElement, parentElement.firstElementChild);
+ } else {
+ document.head.appendChild(styleElement);
+ }
+ } else {
+ reject(new Error(`Couldn't find parent element`));
+
+ return;
+ }
+
+ resolve();
+ });
+ }
+
+ /**
+ * Helper function for WebViewExt.executePromise(scriptCode).
+ */
+ public async executePromise(promise: Promise, eventName: string) {
+ try {
+ const data = await promise;
+ this.emit(eventName, {
+ data,
+ });
+ } catch (err) {
+ this.emitError(err, eventName);
+ }
+ }
+
+ /**
+ * Emit an error to the native-layer.
+ * This is used to workaround the problem of serializing an Error-object.
+ * If err is an Error the message and stack will be extracted and emitted to the native-layer.
+ */
+ public emitError(err: any, eventName = 'web-error') {
+ if (typeof err === 'object' && err?.message) {
+ // Error objects cannot be serialized
+ this.emit(eventName, {
+ err: this.serializeError(err),
+ });
+ } else {
+ this.emit(eventName, {
+ err,
+ });
+ }
+ }
+
+ private elementIdFromHref(href: string) {
+ return href.replace(/^[:]*:\/\//, '').replace(/[^a-z0-9]/g, '');
+ }
+
+ /**
+ * Error objects cannot be serialized properly.
+ */
+ private serializeError(error: any) {
+ const { name, message, stack } = error;
+ const res = {
+ name,
+ message,
+ stack,
+ };
+
+ for (const [key, value] of Object.entries(error)) {
+ if (value instanceof HTMLElement) {
+ continue;
+ }
+
+ if (!(key in res)) {
+ res[key] = value;
+ }
+ }
+
+ return res;
+ }
+}
+
+const nsBridgeReadyEventName = 'ns-bridge-ready';
+
+if (!('nsWebViewBridge' in window)) {
+ ((fn) => {
+ // Only create the NSWebViewBridge, if is doesn't already exist.
+ window['nsWebViewBridge'] = new NSWebViewBridge();
+
+ // see if DOM is already available
+ if (document.readyState === 'complete' || document.readyState === 'interactive') {
+ // call on next available tick
+ setTimeout(fn, 1);
+ } else {
+ document.addEventListener('DOMContentLoaded', fn);
+ }
+ })(() => {
+ // Handler old spelling error in event name...
+ for (const eventName of [nsBridgeReadyEventName, `ns-brige-ready`]) {
+ if (typeof CustomEvent !== 'undefined') {
+ window.dispatchEvent(
+ new CustomEvent(eventName, {
+ detail: window['nsWebViewBridge'],
+ })
+ );
+ } else {
+ window.dispatchEvent(new Event(eventName));
+ }
+ }
+ });
+}
diff --git a/tools/scripts/www-src/package.json b/tools/scripts/www-src/package.json
new file mode 100644
index 0000000..15a0f0c
--- /dev/null
+++ b/tools/scripts/www-src/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "www-src",
+ "private": false,
+ "version": "0.0.0",
+ "description": "",
+ "main": "ns-webview-bridge.js",
+ "scripts": {
+ "build": "tsc -p tsconfig.json"
+ },
+ "license": "MIT"
+}
diff --git a/tools/scripts/www-src/tsconfig.json b/tools/scripts/www-src/tsconfig.json
new file mode 100644
index 0000000..fb149a6
--- /dev/null
+++ b/tools/scripts/www-src/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "outDir": "../www",
+ "allowUnreachableCode": false,
+ "allowUnusedLabels": false,
+ "target": "es5",
+ "module": "commonjs",
+ "lib": ["dom", "es2015", "ES2017.Object"],
+ "removeComments": true,
+ "downlevelIteration": true,
+ "noUnusedLocals": true,
+ "strictNullChecks": true
+ }
+}
diff --git a/tools/scripts/www/metadata-view-port.js b/tools/scripts/www/metadata-view-port.js
new file mode 100644
index 0000000..ec316b1
--- /dev/null
+++ b/tools/scripts/www/metadata-view-port.js
@@ -0,0 +1,50 @@
+(function (window) {
+ var defaultViewPort = {
+ initialScale: 1.0,
+ };
+ var document = window.document;
+ var meta = document.querySelector('head meta[name="viewport"]');
+ if (!meta) {
+ meta = document.createElement('meta');
+ meta.setAttribute('name', 'viewport');
+ document.head.appendChild(meta);
+ }
+ var viewPortInput = '<%= VIEW_PORT %>';
+ var viewPortValues = defaultViewPort;
+ if (viewPortInput && typeof viewPortInput === 'object') {
+ viewPortValues = viewPortInput;
+ }
+ var _a = viewPortValues.initialScale,
+ initialScale = _a === void 0 ? defaultViewPort.initialScale : _a,
+ width = viewPortValues.width,
+ height = viewPortValues.height,
+ userScalable = viewPortValues.userScalable,
+ minimumScale = viewPortValues.minimumScale,
+ maximumScale = viewPortValues.maximumScale;
+ var content = ['initial-scale='.concat(initialScale)];
+ if (width) {
+ content.push('width='.concat(width));
+ }
+ if (height) {
+ content.push('height='.concat(height));
+ }
+ if (typeof userScalable === 'boolean') {
+ content.push('user-scalable='.concat(userScalable ? 'yes' : 'no'));
+ } else if (typeof userScalable === 'string') {
+ var lcUserScalable = ''.concat(userScalable).toLowerCase();
+ if (lcUserScalable === 'yes') {
+ content.push('user-scalable=yes');
+ } else if (lcUserScalable === 'no') {
+ content.push('user-scalable=no');
+ } else {
+ console.error('userScalable='.concat(JSON.stringify(userScalable), ' is an unknown value'));
+ }
+ }
+ if (minimumScale) {
+ content.push('minimum-scale='.concat(minimumScale));
+ }
+ if (maximumScale) {
+ content.push('maximum-scale='.concat(maximumScale));
+ }
+ meta.setAttribute('content', content.join(', '));
+})(window);
diff --git a/tools/scripts/www/ns-webview-bridge.js b/tools/scripts/www/ns-webview-bridge.js
new file mode 100644
index 0000000..2e5c926
--- /dev/null
+++ b/tools/scripts/www/ns-webview-bridge.js
@@ -0,0 +1,525 @@
+var __awaiter =
+ (this && this.__awaiter) ||
+ function (thisArg, _arguments, P, generator) {
+ function adopt(value) {
+ return value instanceof P
+ ? value
+ : new P(function (resolve) {
+ resolve(value);
+ });
+ }
+ return new (P || (P = Promise))(function (resolve, reject) {
+ function fulfilled(value) {
+ try {
+ step(generator.next(value));
+ } catch (e) {
+ reject(e);
+ }
+ }
+ function rejected(value) {
+ try {
+ step(generator['throw'](value));
+ } catch (e) {
+ reject(e);
+ }
+ }
+ function step(result) {
+ result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
+ }
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
+ });
+ };
+var __generator =
+ (this && this.__generator) ||
+ function (thisArg, body) {
+ var _ = {
+ label: 0,
+ sent: function () {
+ if (t[0] & 1) throw t[1];
+ return t[1];
+ },
+ trys: [],
+ ops: [],
+ },
+ f,
+ y,
+ t,
+ g;
+ return (
+ (g = { next: verb(0), throw: verb(1), return: verb(2) }),
+ typeof Symbol === 'function' &&
+ (g[Symbol.iterator] = function () {
+ return this;
+ }),
+ g
+ );
+ function verb(n) {
+ return function (v) {
+ return step([n, v]);
+ };
+ }
+ function step(op) {
+ if (f) throw new TypeError('Generator is already executing.');
+ while ((g && ((g = 0), op[0] && (_ = 0)), _))
+ try {
+ if (((f = 1), y && (t = op[0] & 2 ? y['return'] : op[0] ? y['throw'] || ((t = y['return']) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done)) return t;
+ if (((y = 0), t)) op = [op[0] & 2, t.value];
+ switch (op[0]) {
+ case 0:
+ case 1:
+ t = op;
+ break;
+ case 4:
+ _.label++;
+ return { value: op[1], done: false };
+ case 5:
+ _.label++;
+ y = op[1];
+ op = [0];
+ continue;
+ case 7:
+ op = _.ops.pop();
+ _.trys.pop();
+ continue;
+ default:
+ if (!((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && (op[0] === 6 || op[0] === 2)) {
+ _ = 0;
+ continue;
+ }
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) {
+ _.label = op[1];
+ break;
+ }
+ if (op[0] === 6 && _.label < t[1]) {
+ _.label = t[1];
+ t = op;
+ break;
+ }
+ if (t && _.label < t[2]) {
+ _.label = t[2];
+ _.ops.push(op);
+ break;
+ }
+ if (t[2]) _.ops.pop();
+ _.trys.pop();
+ continue;
+ }
+ op = body.call(thisArg, _);
+ } catch (e) {
+ op = [6, e];
+ y = 0;
+ } finally {
+ f = t = 0;
+ }
+ if (op[0] & 5) throw op[1];
+ return { value: op[0] ? op[1] : void 0, done: true };
+ }
+ };
+var __values =
+ (this && this.__values) ||
+ function (o) {
+ var s = typeof Symbol === 'function' && Symbol.iterator,
+ m = s && o[s],
+ i = 0;
+ if (m) return m.call(o);
+ if (o && typeof o.length === 'number')
+ return {
+ next: function () {
+ if (o && i >= o.length) o = void 0;
+ return { value: o && o[i++], done: !o };
+ },
+ };
+ throw new TypeError(s ? 'Object is not iterable.' : 'Symbol.iterator is not defined.');
+ };
+var __read =
+ (this && this.__read) ||
+ function (o, n) {
+ var m = typeof Symbol === 'function' && o[Symbol.iterator];
+ if (!m) return o;
+ var i = m.call(o),
+ r,
+ ar = [],
+ e;
+ try {
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
+ } catch (error) {
+ e = { error: error };
+ } finally {
+ try {
+ if (r && !r.done && (m = i['return'])) m.call(i);
+ } finally {
+ if (e) throw e.error;
+ }
+ }
+ return ar;
+ };
+if (!Object.keys) {
+ Object.keys = (function () {
+ 'use strict';
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
+ var hasDontEnumBug = !{ toString: null }.propertyIsEnumerable('toString');
+ var dontEnums = ['toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'constructor'];
+ var dontEnumsLength = dontEnums.length;
+ return function (obj) {
+ if (typeof obj !== 'function' && (typeof obj !== 'object' || obj === null)) {
+ throw new TypeError('Object.keys called on non-object');
+ }
+ var result = new Array();
+ for (var prop in obj) {
+ if (hasOwnProperty.call(obj, prop)) {
+ result.push(prop);
+ }
+ }
+ if (hasDontEnumBug) {
+ for (var i = 0; i < dontEnumsLength; i++) {
+ if (hasOwnProperty.call(obj, dontEnums[i])) {
+ result.push(dontEnums[i]);
+ }
+ }
+ }
+ return result;
+ };
+ })();
+}
+if (!Object.entries) {
+ Object.entries = function (obj) {
+ var ownProps = Object.keys(obj);
+ var i = ownProps.length;
+ var resArray = new Array(i);
+ while (i--) {
+ resArray[i] = [ownProps[i], obj[ownProps[i]]];
+ }
+ return resArray;
+ };
+}
+function getWkWebViewMessageHandler() {
+ var _a, _b;
+ var w = window;
+ if (!((_b = (_a = w === null || w === void 0 ? void 0 : w.webkit) === null || _a === void 0 ? void 0 : _a.messageHandlers) === null || _b === void 0 ? void 0 : _b.nsBridge)) {
+ console.error("Cannot get the window.webkit.messageHandlers.nsBridge - we can't communicate with native-layer");
+ return;
+ }
+ return w.webkit.messageHandlers.nsBridge;
+}
+var NSWebViewBridge = (function () {
+ function NSWebViewBridge() {
+ this.eventListenerMap = {};
+ }
+ Object.defineProperty(NSWebViewBridge.prototype, 'androidWebViewBridge', {
+ get: function () {
+ if (typeof androidWebViewBridge !== 'undefined') {
+ return androidWebViewBridge;
+ }
+ },
+ enumerable: false,
+ configurable: true,
+ });
+ NSWebViewBridge.prototype.onNativeEvent = function (eventName, data) {
+ var e_1, _a;
+ var events = this.eventListenerMap[eventName];
+ if (!(events === null || events === void 0 ? void 0 : events.length)) {
+ return;
+ }
+ try {
+ for (var events_1 = __values(events), events_1_1 = events_1.next(); !events_1_1.done; events_1_1 = events_1.next()) {
+ var listener = events_1_1.value;
+ var res = listener === null || listener === void 0 ? void 0 : listener(data);
+ if (res === false) {
+ break;
+ }
+ }
+ } catch (e_1_1) {
+ e_1 = { error: e_1_1 };
+ } finally {
+ try {
+ if (events_1_1 && !events_1_1.done && (_a = events_1.return)) _a.call(events_1);
+ } finally {
+ if (e_1) throw e_1.error;
+ }
+ }
+ };
+ NSWebViewBridge.prototype.emitEventToIOS = function (eventName, data) {
+ var messageHandler = getWkWebViewMessageHandler();
+ if (messageHandler) {
+ messageHandler.postMessage(
+ JSON.stringify({
+ eventName: eventName,
+ data: data,
+ })
+ );
+ return;
+ }
+ console.error('NSWebViewBridge only supports WKWebView');
+ };
+ NSWebViewBridge.prototype.emitEventToAndroid = function (eventName, data) {
+ var androidWebViewBridge = this.androidWebViewBridge;
+ if (!androidWebViewBridge) {
+ console.error('Tried to emit to android without the androidWebViewBridge');
+ return;
+ }
+ androidWebViewBridge.emitEvent(eventName, data !== null && data !== void 0 ? data : 'null');
+ };
+ NSWebViewBridge.prototype.on = function (eventName, callback) {
+ if (!callback) {
+ return;
+ }
+ if (!this.eventListenerMap[eventName]) {
+ this.eventListenerMap[eventName] = [];
+ }
+ this.eventListenerMap[eventName].push(callback);
+ };
+ NSWebViewBridge.prototype.addEventListener = function (eventName, callback) {
+ this.on(eventName, callback);
+ };
+ NSWebViewBridge.prototype.off = function (eventName, callback) {
+ if (!eventName) {
+ this.eventListenerMap = {};
+ return;
+ }
+ if (!this.eventListenerMap[eventName]) {
+ return;
+ }
+ if (!callback) {
+ delete this.eventListenerMap[eventName];
+ return;
+ }
+ this.eventListenerMap[eventName] = this.eventListenerMap[eventName].filter(function (oldCallback) {
+ return oldCallback !== callback;
+ });
+ if (this.eventListenerMap[eventName].length === 0) {
+ delete this.eventListenerMap[eventName];
+ }
+ };
+ NSWebViewBridge.prototype.removeEventListener = function (eventName, callback) {
+ return this.off(eventName, callback);
+ };
+ NSWebViewBridge.prototype.emit = function (eventName, data) {
+ if (this.androidWebViewBridge) {
+ this.emitEventToAndroid(eventName, JSON.stringify(data));
+ } else {
+ this.emitEventToIOS(eventName, data);
+ }
+ };
+ NSWebViewBridge.prototype.injectJavaScriptFile = function (href) {
+ var _this = this;
+ var elId = this.elementIdFromHref(href);
+ if (document.getElementById(elId)) {
+ console.log(''.concat(elId, ' already exists'));
+ return Promise.resolve();
+ }
+ return new Promise(function (resolve, reject) {
+ var scriptElement = document.createElement('script');
+ scriptElement.async = true;
+ scriptElement.setAttribute('id', elId);
+ scriptElement.addEventListener('error', function (error) {
+ console.error('Failed to load '.concat(href, ' - error: ').concat(error));
+ reject(_this.serializeError(error));
+ if (scriptElement.parentElement) {
+ scriptElement.parentElement.removeChild(scriptElement);
+ }
+ });
+ scriptElement.addEventListener('load', function () {
+ console.info('Loaded '.concat(href));
+ window.requestAnimationFrame(function () {
+ resolve();
+ });
+ if (scriptElement.parentElement) {
+ scriptElement.parentElement.removeChild(scriptElement);
+ }
+ });
+ scriptElement.src = href;
+ document.body.appendChild(scriptElement);
+ });
+ };
+ NSWebViewBridge.prototype.injectJavaScript = function (elId, scriptCode) {
+ if (document.getElementById(elId)) {
+ console.log(''.concat(elId, ' already exists'));
+ return Promise.resolve();
+ }
+ return new Promise(function (resolve, reject) {
+ var scriptElement = document.createElement('script');
+ scriptElement.setAttribute('id', elId);
+ scriptElement.addEventListener('error', function (error) {
+ console.error('Failed to inject javascript- error: '.concat(error));
+ reject(error);
+ if (scriptElement.parentElement) {
+ scriptElement.parentElement.removeChild(scriptElement);
+ }
+ });
+ scriptElement.text = scriptCode;
+ document.body.appendChild(scriptElement);
+ window.requestAnimationFrame(function () {
+ return resolve();
+ });
+ });
+ };
+ NSWebViewBridge.prototype.injectStyleSheetFile = function (href, insertBefore) {
+ var elId = this.elementIdFromHref(href);
+ if (document.getElementById(elId)) {
+ console.log(''.concat(elId, ' already exists'));
+ return Promise.resolve();
+ }
+ return new Promise(function (resolve, reject) {
+ var linkElement = document.createElement('link');
+ linkElement.addEventListener('error', function (error) {
+ console.error('Failed to load '.concat(href, ' - error: ').concat(error));
+ reject(error);
+ if (linkElement.parentElement) {
+ linkElement.parentElement.removeChild(linkElement);
+ }
+ });
+ linkElement.addEventListener('load', function () {
+ console.info('Loaded '.concat(href));
+ window.requestAnimationFrame(function () {
+ resolve();
+ });
+ });
+ linkElement.setAttribute('id', elId);
+ linkElement.setAttribute('rel', 'stylesheet');
+ linkElement.setAttribute('type', 'text/css');
+ linkElement.setAttribute('href', href);
+ if (document.head) {
+ if (insertBefore && document.head.childElementCount > 0) {
+ document.head.insertBefore(linkElement, document.head.firstElementChild);
+ } else {
+ document.head.appendChild(linkElement);
+ }
+ }
+ });
+ };
+ NSWebViewBridge.prototype.injectStyleSheet = function (elId, stylesheet, insertBefore) {
+ if (document.getElementById(elId)) {
+ console.log(''.concat(elId, ' already exists'));
+ return Promise.resolve();
+ }
+ return new Promise(function (resolve, reject) {
+ var _a;
+ var styleElement = document.createElement('style');
+ styleElement.addEventListener('error', reject);
+ styleElement.textContent = stylesheet;
+ styleElement.setAttribute('id', elId);
+ var parentElement = (_a = document.head) !== null && _a !== void 0 ? _a : document.body;
+ if (parentElement) {
+ if (insertBefore && parentElement.childElementCount > 0) {
+ document.head.insertBefore(styleElement, parentElement.firstElementChild);
+ } else {
+ document.head.appendChild(styleElement);
+ }
+ } else {
+ reject(new Error("Couldn't find parent element"));
+ return;
+ }
+ resolve();
+ });
+ };
+ NSWebViewBridge.prototype.executePromise = function (promise, eventName) {
+ return __awaiter(this, void 0, void 0, function () {
+ var data, err_1;
+ return __generator(this, function (_a) {
+ switch (_a.label) {
+ case 0:
+ _a.trys.push([0, 2, , 3]);
+ return [4, promise];
+ case 1:
+ data = _a.sent();
+ this.emit(eventName, {
+ data: data,
+ });
+ return [3, 3];
+ case 2:
+ err_1 = _a.sent();
+ this.emitError(err_1, eventName);
+ return [3, 3];
+ case 3:
+ return [2];
+ }
+ });
+ });
+ };
+ NSWebViewBridge.prototype.emitError = function (err, eventName) {
+ if (eventName === void 0) {
+ eventName = 'web-error';
+ }
+ if (typeof err === 'object' && (err === null || err === void 0 ? void 0 : err.message)) {
+ this.emit(eventName, {
+ err: this.serializeError(err),
+ });
+ } else {
+ this.emit(eventName, {
+ err: err,
+ });
+ }
+ };
+ NSWebViewBridge.prototype.elementIdFromHref = function (href) {
+ return href.replace(/^[:]*:\/\//, '').replace(/[^a-z0-9]/g, '');
+ };
+ NSWebViewBridge.prototype.serializeError = function (error) {
+ var e_2, _a;
+ var name = error.name,
+ message = error.message,
+ stack = error.stack;
+ var res = {
+ name: name,
+ message: message,
+ stack: stack,
+ };
+ try {
+ for (var _b = __values(Object.entries(error)), _c = _b.next(); !_c.done; _c = _b.next()) {
+ var _d = __read(_c.value, 2),
+ key = _d[0],
+ value = _d[1];
+ if (value instanceof HTMLElement) {
+ continue;
+ }
+ if (!(key in res)) {
+ res[key] = value;
+ }
+ }
+ } catch (e_2_1) {
+ e_2 = { error: e_2_1 };
+ } finally {
+ try {
+ if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
+ } finally {
+ if (e_2) throw e_2.error;
+ }
+ }
+ return res;
+ };
+ return NSWebViewBridge;
+})();
+var nsBridgeReadyEventName = 'ns-bridge-ready';
+if (!('nsWebViewBridge' in window)) {
+ (function (fn) {
+ window['nsWebViewBridge'] = new NSWebViewBridge();
+ if (document.readyState === 'complete' || document.readyState === 'interactive') {
+ setTimeout(fn, 1);
+ } else {
+ document.addEventListener('DOMContentLoaded', fn);
+ }
+ })(function () {
+ var e_3, _a;
+ try {
+ for (var _b = __values([nsBridgeReadyEventName, 'ns-brige-ready']), _c = _b.next(); !_c.done; _c = _b.next()) {
+ var eventName = _c.value;
+ if (typeof CustomEvent !== 'undefined') {
+ window.dispatchEvent(
+ new CustomEvent(eventName, {
+ detail: window['nsWebViewBridge'],
+ })
+ );
+ } else {
+ window.dispatchEvent(new Event(eventName));
+ }
+ }
+ } catch (e_3_1) {
+ e_3 = { error: e_3_1 };
+ } finally {
+ try {
+ if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
+ } finally {
+ if (e_3) throw e_3.error;
+ }
+ }
+ });
+}
diff --git a/tools/workspace-scripts.js b/tools/workspace-scripts.js
index a42197d..ae55742 100644
--- a/tools/workspace-scripts.js
+++ b/tools/workspace-scripts.js
@@ -103,6 +103,13 @@ module.exports = {
description: '@essent/nativescript-ng-sentry: Build',
},
},
+ // @essent/nativescript-webview-ext
+ 'nativescript-webview-ext': {
+ build: {
+ script: 'nx run nativescript-webview-ext:build.all',
+ description: '@essent/nativescript-webview-ext: Build',
+ },
+ },
'build-all': {
script: 'nx run-many --target=build.all --all',
description: 'Build all packages',
@@ -137,6 +144,10 @@ module.exports = {
script: 'nx run nativescript-ng-sentry:focus',
description: 'Focus on @essent/nativescript-ng-sentry',
},
+ 'nativescript-webview-ext': {
+ script: 'nx run nativescript-webview-ext:focus',
+ description: 'Focus on @essent/nativescript-webview-ext',
+ },
reset: {
script: 'nx g @nativescript/plugin-tools:focus-packages',
description: 'Reset Focus',
diff --git a/tsconfig.base.json b/tsconfig.base.json
index d347cd5..76977c4 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -27,7 +27,9 @@
"@essent/nativescript-iadvize": ["packages/nativescript-iadvize/index.d.ts"],
"@essent/nativescript-medallia": ["packages/nativescript-medallia/index.d.ts"],
"@essent/nativescript-ng-sentry": ["packages/nativescript-ng-sentry/index.d.ts"],
- "@essent/nativescript-urban-airship": ["packages/nativescript-urban-airship/index.d.ts"]
+ "@essent/nativescript-urban-airship": ["packages/nativescript-urban-airship/index.d.ts"],
+ "@essent/nativescript-webview-ext": ["packages/nativescript-webview-ext/index.d.ts"],
+ "@essent/nativescript-webview-ext/angular": ["packages/nativescript-webview-ext/angular/index.ts"]
}
},
"exclude": ["node_modules", "tmp"]