@livx.cc/appwrap 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +20 -0
  3. package/package.json +46 -0
  4. package/runtime/App_Resources/Android/app.gradle +34 -0
  5. package/runtime/App_Resources/Android/before-plugins.gradle +15 -0
  6. package/runtime/App_Resources/Android/src/main/AndroidManifest.xml +76 -0
  7. package/runtime/App_Resources/Android/src/main/res/drawable/ic_launcher_foreground.xml +15 -0
  8. package/runtime/App_Resources/Android/src/main/res/drawable-hdpi/background.png +0 -0
  9. package/runtime/App_Resources/Android/src/main/res/drawable-hdpi/logo.png +0 -0
  10. package/runtime/App_Resources/Android/src/main/res/drawable-ldpi/background.png +0 -0
  11. package/runtime/App_Resources/Android/src/main/res/drawable-ldpi/logo.png +0 -0
  12. package/runtime/App_Resources/Android/src/main/res/drawable-mdpi/background.png +0 -0
  13. package/runtime/App_Resources/Android/src/main/res/drawable-mdpi/logo.png +0 -0
  14. package/runtime/App_Resources/Android/src/main/res/drawable-nodpi/splash_screen.xml +8 -0
  15. package/runtime/App_Resources/Android/src/main/res/drawable-xhdpi/background.png +0 -0
  16. package/runtime/App_Resources/Android/src/main/res/drawable-xhdpi/logo.png +0 -0
  17. package/runtime/App_Resources/Android/src/main/res/drawable-xxhdpi/background.png +0 -0
  18. package/runtime/App_Resources/Android/src/main/res/drawable-xxhdpi/logo.png +0 -0
  19. package/runtime/App_Resources/Android/src/main/res/drawable-xxxhdpi/background.png +0 -0
  20. package/runtime/App_Resources/Android/src/main/res/drawable-xxxhdpi/logo.png +0 -0
  21. package/runtime/App_Resources/Android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  22. package/runtime/App_Resources/Android/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  23. package/runtime/App_Resources/Android/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  24. package/runtime/App_Resources/Android/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  25. package/runtime/App_Resources/Android/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  26. package/runtime/App_Resources/Android/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  27. package/runtime/App_Resources/Android/src/main/res/values/colors.xml +14 -0
  28. package/runtime/App_Resources/Android/src/main/res/values/ic_launcher_background.xml +4 -0
  29. package/runtime/App_Resources/Android/src/main/res/values/styles.xml +42 -0
  30. package/runtime/App_Resources/Android/src/main/res/values-v21/colors.xml +5 -0
  31. package/runtime/App_Resources/Android/src/main/res/values-v21/styles.xml +32 -0
  32. package/runtime/App_Resources/Android/src/main/res/values-v29/styles.xml +18 -0
  33. package/runtime/App_Resources/Android/src/main/res/xml/file_paths.xml +5 -0
  34. package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
  35. package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-1024.png +0 -0
  36. package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-20.png +0 -0
  37. package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png +0 -0
  38. package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png +0 -0
  39. package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29.png +0 -0
  40. package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png +0 -0
  41. package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png +0 -0
  42. package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40.png +0 -0
  43. package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png +0 -0
  44. package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png +0 -0
  45. package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png +0 -0
  46. package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png +0 -0
  47. package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76.png +0 -0
  48. package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png +0 -0
  49. package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png +0 -0
  50. package/runtime/App_Resources/iOS/Assets.xcassets/Contents.json +6 -0
  51. package/runtime/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/Contents.json +23 -0
  52. package/runtime/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill.png +0 -0
  53. package/runtime/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill@2x.png +0 -0
  54. package/runtime/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill@3x.png +0 -0
  55. package/runtime/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/Contents.json +23 -0
  56. package/runtime/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center.png +0 -0
  57. package/runtime/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center@2x.png +0 -0
  58. package/runtime/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center@3x.png +0 -0
  59. package/runtime/App_Resources/iOS/Info.plist +60 -0
  60. package/runtime/App_Resources/iOS/LaunchScreen.storyboard +53 -0
  61. package/runtime/App_Resources/iOS/Podfile +13 -0
  62. package/runtime/App_Resources/iOS/PrivacyInfo.xcprivacy +59 -0
  63. package/runtime/App_Resources/iOS/build.xcconfig +7 -0
  64. package/runtime/app/app.ts +58 -0
  65. package/runtime/app/main-page.ts +116 -0
  66. package/runtime/app/main-page.xml +10 -0
  67. package/runtime/app/shell/active-modules.generated.ts +9 -0
  68. package/runtime/app/shell/android-helpers.ts +76 -0
  69. package/runtime/app/shell/bridge.ts +99 -0
  70. package/runtime/app/shell/capabilities.manifest.ts +207 -0
  71. package/runtime/app/shell/config.ts +35 -0
  72. package/runtime/app/shell/custom-webview.android.ts +270 -0
  73. package/runtime/app/shell/custom-webview.ios.ts +258 -0
  74. package/runtime/app/shell/custom-webview.ts +17 -0
  75. package/runtime/app/shell/env.ts +81 -0
  76. package/runtime/app/shell/events.ts +100 -0
  77. package/runtime/app/shell/fcm-bootstrap.generated.ts +1 -0
  78. package/runtime/app/shell/fcm-firebase.d.ts +16 -0
  79. package/runtime/app/shell/fcm-service.android.ts +40 -0
  80. package/runtime/app/shell/fcm-service.ios.ts +3 -0
  81. package/runtime/app/shell/handlers-android.ts +417 -0
  82. package/runtime/app/shell/handlers-billing.ts +197 -0
  83. package/runtime/app/shell/handlers-extended.ts +405 -0
  84. package/runtime/app/shell/handlers-health.ts +134 -0
  85. package/runtime/app/shell/handlers-media.ts +126 -0
  86. package/runtime/app/shell/handlers-oauth.ts +69 -0
  87. package/runtime/app/shell/handlers-parity.ts +257 -0
  88. package/runtime/app/shell/handlers-push.ts +189 -0
  89. package/runtime/app/shell/handlers-system.ts +70 -0
  90. package/runtime/app/shell/handlers.ts +149 -0
  91. package/runtime/app/shell/ios-frameworks.d.ts +36 -0
  92. package/runtime/app/shell/ios-image.ts +15 -0
  93. package/runtime/app/shell/ios-ui-delegate.ts +131 -0
  94. package/runtime/app/shell/mime.ts +29 -0
  95. package/runtime/app/shell/native-log.ts +22 -0
  96. package/runtime/app/shell/optional-handlers.generated.ts +10 -0
  97. package/runtime/app/shell/orientation.ts +43 -0
  98. package/runtime/app/shell/status-bar.ts +129 -0
  99. package/runtime/app/shell/toast.ts +67 -0
  100. package/runtime/app/shell/web-quirks.ts +131 -0
  101. package/runtime/modules-native/health/App_Resources/Android/src/main/java/cc/livx/appwrap/HealthConnectBridge.kt +74 -0
  102. package/runtime/nativescript.config.ts +15 -0
  103. package/runtime/package.json +21 -0
  104. package/runtime/references.d.ts +2 -0
  105. package/runtime/tsconfig.json +19 -0
  106. package/runtime/webpack.config.js +27 -0
  107. package/scripts/stage-assets.mjs +21 -0
  108. package/src/cli.ts +1328 -0
  109. package/templates/ci/fastlane/Fastfile +43 -0
  110. package/templates/ci/fastlane/Matchfile +3 -0
  111. package/templates/ci/github/workflows/appwrap-pr.yml +28 -0
  112. package/templates/ci/github/workflows/appwrap-release-android.yml +51 -0
  113. package/templates/ci/github/workflows/appwrap-release-ios.yml +41 -0
@@ -0,0 +1,149 @@
1
+ import { ApplicationSettings, Utils, isAndroid, isIOS } from '@nativescript/core';
2
+ import { bridge } from './bridge';
3
+ import { SHELL_CONFIG } from './config';
4
+ import { onPwaHandshake } from './events';
5
+ import { showToast } from './toast';
6
+ import { setStatusBarStyle } from './status-bar';
7
+ import { buildCapabilityMap } from './capabilities.manifest';
8
+ import { ACTIVE_MODULE_NAMES } from './active-modules.generated';
9
+
10
+ /** Build identifier for the native shell bundle — bump per deploy to spot stale bundles. */
11
+ export const SHELL_BUILD = 'oauth-media-speaker-1';
12
+
13
+ /** Register all protocol-v1 handlers. */
14
+ export function registerHandlers(): void {
15
+ bridge.register('app.handshake', () => {
16
+ // The PWA's JS is now live → flush any deep link buffered during launch.
17
+ onPwaHandshake();
18
+ // Capability map is composed from the active module set (appwrap.json `modules` → the generated
19
+ // ACTIVE_MODULE_NAMES) + the always-on core, deduped. Opt-out capabilities report 'none' so the
20
+ // kit degrades gracefully. Push is special — gated by per-platform build config, not the manifest.
21
+ const capabilities = buildCapabilityMap(new Set(ACTIVE_MODULE_NAMES), isIOS ? 'ios' : 'android') as Record<string, 'native' | 'none'>;
22
+ capabilities.push = (isIOS ? SHELL_CONFIG.pushIos : SHELL_CONFIG.pushAndroid) ? 'native' : 'none';
23
+ return {
24
+ protocol: 1,
25
+ platform: isIOS ? 'ios' : 'android',
26
+ app: { id: SHELL_CONFIG.appId, name: SHELL_CONFIG.name, version: SHELL_CONFIG.version, build: SHELL_BUILD },
27
+ debug: { lastNotifTap: safeJson(ApplicationSettings.getString('kit:__notifTap', '')) },
28
+ capabilities,
29
+ };
30
+ });
31
+
32
+ bridge.register('haptics.impact', ({ style = 'medium' }: { style?: string }) => {
33
+ if (isIOS) {
34
+ const styles: Record<string, UIImpactFeedbackStyle> = {
35
+ light: UIImpactFeedbackStyle.Light,
36
+ medium: UIImpactFeedbackStyle.Medium,
37
+ heavy: UIImpactFeedbackStyle.Heavy,
38
+ soft: UIImpactFeedbackStyle.Soft,
39
+ rigid: UIImpactFeedbackStyle.Rigid,
40
+ };
41
+ const generator = UIImpactFeedbackGenerator.alloc().initWithStyle(
42
+ styles[style] ?? UIImpactFeedbackStyle.Medium
43
+ );
44
+ generator.impactOccurred();
45
+ } else if (isAndroid) {
46
+ vibrateAndroid(style === 'heavy' ? 60 : style === 'light' ? 15 : 30);
47
+ }
48
+ });
49
+
50
+ bridge.register('haptics.notify', ({ type = 'success' }: { type?: string }) => {
51
+ if (isIOS) {
52
+ const types: Record<string, UINotificationFeedbackType> = {
53
+ success: UINotificationFeedbackType.Success,
54
+ warning: UINotificationFeedbackType.Warning,
55
+ error: UINotificationFeedbackType.Error,
56
+ };
57
+ const gen = UINotificationFeedbackGenerator.alloc().init();
58
+ gen.notificationOccurred(types[type] ?? UINotificationFeedbackType.Success);
59
+ } else if (isAndroid) {
60
+ vibrateAndroid(type === 'error' ? 120 : 60);
61
+ }
62
+ });
63
+
64
+ bridge.register('share.share', async ({ title, text, url }: { title?: string; text?: string; url?: string }) => {
65
+ const content = [text, url].filter(Boolean).join('\n');
66
+ if (isIOS) {
67
+ const items = NSMutableArray.new();
68
+ if (content) items.addObject(content);
69
+ const controller = UIActivityViewController.alloc().initWithActivityItemsApplicationActivities(
70
+ items as any,
71
+ null
72
+ );
73
+ const rootVC = Utils.ios.getRootViewController();
74
+ // iPad requires a popover anchor
75
+ if (controller.popoverPresentationController) {
76
+ controller.popoverPresentationController.sourceView = rootVC.view;
77
+ }
78
+ rootVC.presentViewControllerAnimatedCompletion(controller, true, null);
79
+ } else if (isAndroid) {
80
+ const intent = new android.content.Intent(android.content.Intent.ACTION_SEND);
81
+ intent.setType('text/plain');
82
+ if (title) intent.putExtra(android.content.Intent.EXTRA_SUBJECT, title);
83
+ intent.putExtra(android.content.Intent.EXTRA_TEXT, content);
84
+ const chooser = android.content.Intent.createChooser(intent, title ?? 'Share');
85
+ chooser.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
86
+ Utils.android.getApplicationContext().startActivity(chooser);
87
+ }
88
+ });
89
+
90
+ bridge.register(
91
+ 'share.files',
92
+ ({ files, text }: { files: Array<{ name: string; mimeType: string; base64: string }>; title?: string; text?: string }) => {
93
+ if (!isIOS) return; // Android file share needs a FileProvider (parked)
94
+ const items = NSMutableArray.new();
95
+ if (text) items.addObject(text);
96
+ const tmp = NSTemporaryDirectory();
97
+ (files ?? []).forEach((f, i) => {
98
+ const data = NSData.alloc().initWithBase64EncodedStringOptions(f.base64, 0 as unknown as NSDataBase64DecodingOptions);
99
+ if (!data) return;
100
+ const filePath = tmp + i + '-' + (f.name || 'file'); // index-prefix: avoid same-name collisions in one call
101
+ data.writeToFileAtomically(filePath, true);
102
+ items.addObject(NSURL.fileURLWithPath(filePath));
103
+ });
104
+ const controller = UIActivityViewController.alloc().initWithActivityItemsApplicationActivities(
105
+ items as any,
106
+ null
107
+ );
108
+ const rootVC = Utils.ios.getRootViewController();
109
+ if (controller.popoverPresentationController) {
110
+ controller.popoverPresentationController.sourceView = rootVC.view; // iPad anchor
111
+ }
112
+ rootVC.presentViewControllerAnimatedCompletion(controller, true, null);
113
+ }
114
+ );
115
+
116
+ bridge.register('storage.get', ({ key }: { key: string }) =>
117
+ JSON.parse(ApplicationSettings.getString(`kit:${key}`, 'null'))
118
+ );
119
+ bridge.register('storage.set', ({ key, value }: { key: string; value?: unknown }) =>
120
+ ApplicationSettings.setString(`kit:${key}`, JSON.stringify(value ?? null))
121
+ );
122
+ bridge.register('storage.remove', ({ key }: { key: string }) =>
123
+ ApplicationSettings.remove(`kit:${key}`)
124
+ );
125
+
126
+ bridge.register('toast.show', ({ message, duration }: { message: string; duration?: 'short' | 'long' }) =>
127
+ showToast(String(message ?? ''), duration ?? 'short')
128
+ );
129
+
130
+ bridge.register('ui.statusBar.setStyle', ({ style }: { style: 'light' | 'dark' }) =>
131
+ setStatusBarStyle(style)
132
+ );
133
+ }
134
+
135
+ /** Parse a stored JSON breadcrumb; null if absent/unparseable (diagnostic, never throws). */
136
+ function safeJson(s: string): unknown {
137
+ if (!s) return null;
138
+ try {
139
+ return JSON.parse(s);
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+
145
+ function vibrateAndroid(ms: number): void {
146
+ const context = Utils.android.getApplicationContext();
147
+ const vibrator = context.getSystemService(android.content.Context.VIBRATOR_SERVICE);
148
+ vibrator?.vibrate(ms);
149
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Ambient declarations for iOS frameworks not covered by the default
3
+ * @nativescript/types-ios reference set (LocalAuthentication, CoreLocation,
4
+ * PhotosUI). Marshalled at runtime by the NativeScript iOS runtime regardless.
5
+ */
6
+ declare const LAContext: any;
7
+ declare const LAPolicy: any;
8
+ declare const LABiometryType: any;
9
+ declare const CLLocationManager: any;
10
+ declare const CLAuthorizationStatus: any;
11
+ declare const CLLocationManagerDelegate: any;
12
+ declare const kCLLocationAccuracyHundredMeters: number;
13
+ declare type CLLocation = any;
14
+ declare type CLLocationManager = any;
15
+ declare const PHPickerConfiguration: any;
16
+ declare const PHPickerViewController: any;
17
+ declare const PHPickerViewControllerDelegate: any;
18
+ declare type PHPickerResult = any;
19
+ declare type PHPickerViewController = any;
20
+ // StoreKit / CoreMotion / ContactsUI / EventKit (parity handlers)
21
+ declare const SKStoreReviewController: any;
22
+ declare const CMMotionManager: any;
23
+ declare const CMPedometer: any;
24
+ // HealthKit (health/steps module)
25
+ declare const HKHealthStore: any;
26
+ declare const HKObjectType: any;
27
+ declare const HKQuery: any;
28
+ declare const HKStatisticsQuery: any;
29
+ declare const HKStatisticsOptions: any;
30
+ declare const HKUnit: any;
31
+ declare const CNContactPickerViewController: any;
32
+ declare const CNContactPickerDelegate: any;
33
+ declare const EKEventStore: any;
34
+ declare const EKEvent: any;
35
+ declare const EKSpan: any;
36
+ declare const EKEntityType: any;
@@ -0,0 +1,15 @@
1
+ /** Downscale a UIImage to `maxSize` (longest edge) and encode as a JPEG data URL. */
2
+ export function uiImageToDataUrl(img: UIImage, maxSize: number): string {
3
+ const w = img.size.width;
4
+ const h = img.size.height;
5
+ const scale = Math.min(1, maxSize / Math.max(w, h));
6
+ const tw = Math.round(w * scale);
7
+ const th = Math.round(h * scale);
8
+ UIGraphicsBeginImageContextWithOptions({ width: tw, height: th } as CGSize, false, 1);
9
+ img.drawInRect({ origin: { x: 0, y: 0 }, size: { width: tw, height: th } } as CGRect);
10
+ const scaled = UIGraphicsGetImageFromCurrentImageContext() ?? img;
11
+ UIGraphicsEndImageContext();
12
+ const data = UIImageJPEGRepresentation(scaled, 0.85);
13
+ const b64 = data ? data.base64EncodedStringWithOptions(0 as unknown as NSDataBase64EncodingOptions) : '';
14
+ return `data:image/jpeg;base64,${b64}`;
15
+ }
@@ -0,0 +1,131 @@
1
+ import { Utils } from '@nativescript/core';
2
+
3
+ /**
4
+ * WKUIDelegate for the shell WebView. Two jobs:
5
+ * 1. Auto-grant getUserMedia (mic/camera) for our own origin — the native app
6
+ * already holds the OS-level permission (Info.plist), so re-prompting per
7
+ * web-origin is friction. iOS 15+ only; older builds fall back to WebKit's
8
+ * own prompt.
9
+ * 2. Re-implement the JS alert/confirm/prompt panels — WKWebView drops them
10
+ * silently without a UIDelegate, which would break any PWA calling
11
+ * window.alert/confirm/prompt.
12
+ *
13
+ * Returns a retained delegate instance (the WKWebView holds uiDelegate weakly).
14
+ */
15
+ export function createUiDelegate(): any {
16
+ const DelegateClass = (NSObject as any).extend(
17
+ {
18
+ // iOS 15+: WKUIDelegate media capture permission.
19
+ webViewRequestMediaCapturePermissionForOriginInitiatedByFrameTypeDecisionHandler(
20
+ _webView: WKWebView,
21
+ _origin: any,
22
+ _frame: any,
23
+ _type: any,
24
+ decisionHandler: (decision: number) => void
25
+ ): void {
26
+ // WKPermissionDecision.Grant === 1 — silently grant (the app already holds the
27
+ // OS-level mic/camera permission; media.ensurePermission established TCC).
28
+ // NOTE: this only ever sees capabilities the build DECLARED — a request for an
29
+ // UNDECLARED capability is rejected upstream in JS (mediaCaptureGuardJs), because
30
+ // WebKit asks TCC to authorize the device AROUND this callback and a missing usage
31
+ // string is a hard process-kill a native `deny` here cannot prevent. So denying
32
+ // here is pointless for the crash; the real guard must be (and is) at the JS layer.
33
+ decisionHandler(1);
34
+ },
35
+
36
+ // Preserve the behavior NS's own UIDelegate gave us (we fully replace it):
37
+ // target=_blank / window.open has no new window in a shell → load it in place.
38
+ webViewCreateWebViewWithConfigurationForNavigationActionWindowFeatures(
39
+ webView: WKWebView,
40
+ _configuration: any,
41
+ navigationAction: WKNavigationAction,
42
+ _windowFeatures: any
43
+ ): WKWebView | null {
44
+ const frame = navigationAction.targetFrame;
45
+ if (!frame || !frame.mainFrame) webView.loadRequest(navigationAction.request);
46
+ return null;
47
+ },
48
+
49
+ webViewRunJavaScriptAlertPanelWithMessageInitiatedByFrameCompletionHandler(
50
+ _webView: WKWebView,
51
+ message: string,
52
+ _frame: any,
53
+ completionHandler: () => void
54
+ ): void {
55
+ Utils.dispatchToMainThread(() => {
56
+ const alert = UIAlertController.alertControllerWithTitleMessagePreferredStyle(
57
+ null as any,
58
+ String(message ?? ''),
59
+ UIAlertControllerStyle.Alert
60
+ );
61
+ alert.addAction(
62
+ UIAlertAction.actionWithTitleStyleHandler('OK', UIAlertActionStyle.Default, () => completionHandler())
63
+ );
64
+ present(alert);
65
+ });
66
+ },
67
+
68
+ webViewRunJavaScriptConfirmPanelWithMessageInitiatedByFrameCompletionHandler(
69
+ _webView: WKWebView,
70
+ message: string,
71
+ _frame: any,
72
+ completionHandler: (ok: boolean) => void
73
+ ): void {
74
+ Utils.dispatchToMainThread(() => {
75
+ const alert = UIAlertController.alertControllerWithTitleMessagePreferredStyle(
76
+ null as any,
77
+ String(message ?? ''),
78
+ UIAlertControllerStyle.Alert
79
+ );
80
+ alert.addAction(
81
+ UIAlertAction.actionWithTitleStyleHandler('Cancel', UIAlertActionStyle.Cancel, () =>
82
+ completionHandler(false)
83
+ )
84
+ );
85
+ alert.addAction(
86
+ UIAlertAction.actionWithTitleStyleHandler('OK', UIAlertActionStyle.Default, () => completionHandler(true))
87
+ );
88
+ present(alert);
89
+ });
90
+ },
91
+
92
+ webViewRunJavaScriptTextInputPanelWithPromptDefaultTextInitiatedByFrameCompletionHandler(
93
+ _webView: WKWebView,
94
+ prompt: string,
95
+ defaultText: string,
96
+ _frame: any,
97
+ completionHandler: (text: string | null) => void
98
+ ): void {
99
+ Utils.dispatchToMainThread(() => {
100
+ const alert = UIAlertController.alertControllerWithTitleMessagePreferredStyle(
101
+ null as any,
102
+ String(prompt ?? ''),
103
+ UIAlertControllerStyle.Alert
104
+ );
105
+ let field: UITextField | null = null;
106
+ alert.addTextFieldWithConfigurationHandler((tf: UITextField) => {
107
+ tf.text = String(defaultText ?? '');
108
+ field = tf;
109
+ });
110
+ alert.addAction(
111
+ UIAlertAction.actionWithTitleStyleHandler('Cancel', UIAlertActionStyle.Cancel, () =>
112
+ completionHandler(null)
113
+ )
114
+ );
115
+ alert.addAction(
116
+ UIAlertAction.actionWithTitleStyleHandler('OK', UIAlertActionStyle.Default, () =>
117
+ completionHandler(field ? String(field.text ?? '') : '')
118
+ )
119
+ );
120
+ present(alert);
121
+ });
122
+ },
123
+ },
124
+ { protocols: [WKUIDelegate] }
125
+ );
126
+ return DelegateClass.new();
127
+ }
128
+
129
+ function present(alert: UIAlertController): void {
130
+ Utils.ios.getRootViewController().presentViewControllerAnimatedCompletion(alert, true, null);
131
+ }
@@ -0,0 +1,29 @@
1
+ /** Extension → MIME map shared by the iOS app:// handler and the Android asset interceptor. */
2
+ export const MIME: Record<string, string> = {
3
+ html: 'text/html',
4
+ js: 'text/javascript',
5
+ mjs: 'text/javascript',
6
+ css: 'text/css',
7
+ json: 'application/json',
8
+ webmanifest: 'application/manifest+json',
9
+ map: 'application/json',
10
+ png: 'image/png',
11
+ jpg: 'image/jpeg',
12
+ jpeg: 'image/jpeg',
13
+ gif: 'image/gif',
14
+ svg: 'image/svg+xml',
15
+ webp: 'image/webp',
16
+ ico: 'image/x-icon',
17
+ woff: 'font/woff',
18
+ woff2: 'font/woff2',
19
+ ttf: 'font/ttf',
20
+ otf: 'font/otf',
21
+ wasm: 'application/wasm',
22
+ txt: 'text/plain',
23
+ mp3: 'audio/mpeg',
24
+ mp4: 'video/mp4',
25
+ };
26
+
27
+ export function mimeFor(ext: string, fallbackPath = ''): string {
28
+ return MIME[ext] ?? (fallbackPath.endsWith('.html') ? 'text/html' : 'application/octet-stream');
29
+ }
@@ -0,0 +1,22 @@
1
+ import { knownFolders, path as nsPath } from '@nativescript/core';
2
+
3
+ declare const NSString: any;
4
+
5
+ /**
6
+ * Single debug log sink → Documents/appwrap-web.log. Both the forwarded WebView console
7
+ * (custom-webview) and native shell diagnostics (handlers) write here, because NativeScript's
8
+ * native `console.log`/`NSLog` do NOT surface to `devicectl --console`/`idevicesyslog` on a device
9
+ * build. Pull it with `appwrap logs ios`. Native lines are tagged `[native:*]`. Capped per session.
10
+ */
11
+ let _log = '';
12
+
13
+ export function appwrapNativeLog(line: string): void {
14
+ try {
15
+ _log += line + '\n';
16
+ if (_log.length > 160000) _log = _log.slice(-120000);
17
+ const p = nsPath.join(knownFolders.documents().path, 'appwrap-web.log');
18
+ NSString.stringWithString(_log).writeToFileAtomicallyEncodingError(p, true, 4 /*NSUTF8*/, null as any);
19
+ } catch {
20
+ /* best-effort */
21
+ }
22
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Generated by `appwrap init`/`sync`. Registers opt-in modules that own their own handler file, so a
3
+ * build that doesn't enable them never imports (or bundles) their native code. Do not edit.
4
+ * Committed DEFAULT registers all optional groups (back-compat). The CLI rewrites the imports/calls
5
+ * to only the active OPTIONAL_GROUPS.
6
+ */
7
+ // Default registers no optional modules (health & friends are strictly opt-in via appwrap.json
8
+ // `modules`). The CLI rewrites this barrel to import only the active OPTIONAL_GROUPS.
9
+ export function registerOptionalHandlers(): void {
10
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * iOS supported-orientation mask, shared between the AppDelegate
3
+ * (`application:supportedInterfaceOrientationsForWindow:`) and the
4
+ * `screen.orientation.*` handlers. Raw UIInterfaceOrientationMask bit values
5
+ * (1 << UIInterfaceOrientation) so no ambient UIKit types are needed.
6
+ */
7
+ export const ORIENTATION_MASK = {
8
+ portrait: 1 << 1, // 2
9
+ portraitUpsideDown: 1 << 2, // 4
10
+ landscapeLeft: 1 << 3, // 8
11
+ landscapeRight: 1 << 4, // 16
12
+ landscape: (1 << 3) | (1 << 4), // 24
13
+ allButUpsideDown: (1 << 1) | (1 << 3) | (1 << 4), // 26 — default, free rotation sans upside-down
14
+ };
15
+
16
+ let iosMask = ORIENTATION_MASK.allButUpsideDown;
17
+
18
+ /** The mask the AppDelegate reports to UIKit. */
19
+ export function iosOrientationMask(): number {
20
+ return iosMask;
21
+ }
22
+
23
+ /** Map a `kit.screen.orientation` lock value to an iOS mask. */
24
+ export function maskForLock(orientation: string): number {
25
+ switch (orientation) {
26
+ case 'portrait':
27
+ return ORIENTATION_MASK.portrait;
28
+ case 'portrait-upside-down':
29
+ return ORIENTATION_MASK.portraitUpsideDown;
30
+ case 'landscape':
31
+ return ORIENTATION_MASK.landscape;
32
+ case 'landscape-left':
33
+ return ORIENTATION_MASK.landscapeLeft;
34
+ case 'landscape-right':
35
+ return ORIENTATION_MASK.landscapeRight;
36
+ default:
37
+ return ORIENTATION_MASK.allButUpsideDown; // 'any' / unlock
38
+ }
39
+ }
40
+
41
+ export function setIosOrientationMask(mask: number): void {
42
+ iosMask = mask;
43
+ }
@@ -0,0 +1,129 @@
1
+ import { Application, Color, Page, Screen, Utils, View, isAndroid, isIOS } from '@nativescript/core';
2
+ import { SHELL_CONFIG } from './config';
3
+
4
+ let currentPage: Page | null = null;
5
+
6
+ /**
7
+ * Make the Android system bars TRANSPARENT so the page background (the NS page `backgroundColor`)
8
+ * spans edge-to-edge under them, instead of the default opaque black status/nav bars that box the
9
+ * WebView in black letterbox strips.
10
+ *
11
+ * Uses NativeScript's own helper (`org.nativescript.widgets.Utils.enableEdgeToEdge` via
12
+ * `Utils.android.enableEdgeToEdge`) with transparent status-bar colors — rolling our own window
13
+ * flags doesn't stick (NS re-applies its layout and its status-bar-style setter clears the
14
+ * translucent flag, giving a black bar or a dark scrim). Required on Android 15+/API 35 anyway.
15
+ *
16
+ * NOTE: NS still insets the WebView itself below the bars (its native widget layout pads the page
17
+ * content), so the bar regions show the PAGE background, not the web body. Consumers should set
18
+ * `appwrap.json.backgroundColor` to their app's theme background so the bars blend seamlessly. (iOS
19
+ * is genuinely edge-to-edge — the WKWebView fills the safe areas and the web paints them via
20
+ * env(safe-area-inset-*).)
21
+ */
22
+ export function enableAndroidEdgeToEdge(): void {
23
+ if (!isAndroid) return;
24
+ const transparent = new Color(0, 0, 0, 0);
25
+ const apply = () => {
26
+ const activity = Application.android?.startActivity;
27
+ if (!activity) return;
28
+ try {
29
+ // Explicit transparent status-bar colors (light + dark) so the page background — not the NS
30
+ // page backgroundColor — shows under the status bar. Nav bar keeps NS's default subtle scrim.
31
+ (Utils as any).android.enableEdgeToEdge(activity, {
32
+ statusBarLightColor: transparent,
33
+ statusBarDarkColor: transparent,
34
+ });
35
+ } catch (e) {
36
+ console.warn('AppWrap: edge-to-edge setup failed', e);
37
+ }
38
+ };
39
+ apply();
40
+ // NS applies its own status-bar inset AFTER onPageLoaded; re-apply on the next loop so edge-to-edge
41
+ // wins the race (otherwise the WebView stays boxed below an opaque status-bar strip).
42
+ setTimeout(apply, 50);
43
+ }
44
+
45
+ /**
46
+ * EXPERIMENTAL (SHELL_CONFIG.edgeToEdge). Make the WebView draw genuinely edge-to-edge UNDER the
47
+ * transparent system bars instead of NS boxing it below them, and feed the real safe-area insets to
48
+ * the web layer as `--saie-{top,right,bottom,left}` CSS vars (dp). With the WebView now filling the
49
+ * window, native `env(safe-area-inset-*)` should also populate — so a PWA can pad with
50
+ * `max(env(safe-area-inset-top), var(--saie-top, 0px))` and paint the bar regions itself, matching
51
+ * any theme (no static page backgroundColor to match). No-op unless edgeToEdge + Android.
52
+ */
53
+ export function wireAndroidSafeArea(webView: View): void {
54
+ if (!isAndroid || !SHELL_CONFIG.edgeToEdge) return;
55
+ // Un-box the layout that wraps the WebView so it (and its child WebView) fills the window UNDER the
56
+ // bars. 'dont-apply' = NS won't pad it with the system-bar insets.
57
+ const layout = (webView.parent as View) ?? webView;
58
+ (layout as any).androidOverflowEdge = 'dont-apply';
59
+
60
+ const density = Screen.mainScreen.scale || 1; // physical px per dp
61
+ const inject = (l: number, t: number, r: number, b: number) => {
62
+ const js =
63
+ `(function(s){` +
64
+ `s.setProperty('--saie-left','${l}px');s.setProperty('--saie-top','${t}px');` +
65
+ `s.setProperty('--saie-right','${r}px');s.setProperty('--saie-bottom','${b}px');` +
66
+ `})(document.documentElement.style);`;
67
+ try {
68
+ (webView as any).android?.evaluateJavascript(js, null);
69
+ } catch (e) {
70
+ console.warn('AppWrap: safe-area inject failed', e);
71
+ }
72
+ };
73
+ inject(0, 0, 0, 0); // sane defaults until the first real read
74
+
75
+ // Read the window insets DIRECTLY from the decor view. NS's androidOverflowInset event doesn't
76
+ // reliably reach a nested layout (its edge-to-edge machinery consumes insets higher up), so we go
77
+ // straight to the platform API — systemBars ∪ displayCutout, the same set a native edge-to-edge
78
+ // app uses. Returns true once non-zero insets are available (i.e. the window is laid out).
79
+ const dp = (px: number) => Math.round((px || 0) / density);
80
+ const readAndInject = (): boolean => {
81
+ try {
82
+ const decor = Application.android?.startActivity?.getWindow()?.getDecorView();
83
+ const wi = decor?.getRootWindowInsets?.();
84
+ if (!wi) return false;
85
+ const Type = (global as any).android.view.WindowInsets.Type;
86
+ const ins = wi.getInsets(Type.systemBars() | Type.displayCutout());
87
+ inject(dp(ins.left), dp(ins.top), dp(ins.right), dp(ins.bottom));
88
+ return ins.top + ins.bottom + ins.left + ins.right > 0;
89
+ } catch (e) {
90
+ console.warn('AppWrap: safe-area read failed', e);
91
+ return false;
92
+ }
93
+ };
94
+
95
+ // Insets are 0 until the window is laid out — retry a few times, then keep them fresh on
96
+ // resume / rotation (bar visibility + cutout side can change).
97
+ let tries = 0;
98
+ const tick = () => {
99
+ if (readAndInject() || ++tries > 12) return;
100
+ setTimeout(tick, 150);
101
+ };
102
+ setTimeout(tick, 80);
103
+ Application.on(Application.resumeEvent, () => setTimeout(readAndInject, 120));
104
+ Application.on(Application.orientationChangedEvent, () => setTimeout(readAndInject, 200));
105
+ }
106
+
107
+ export function bindStatusBarPage(page: Page): void {
108
+ currentPage = page;
109
+ }
110
+
111
+ /** 'light' = white icons/text (for dark backgrounds), 'dark' = black. */
112
+ export function setStatusBarStyle(style: 'light' | 'dark'): void {
113
+ if (isIOS) {
114
+ if (currentPage) (currentPage as any).statusBarStyle = style;
115
+ } else if (isAndroid) {
116
+ const window = Application.android?.startActivity?.getWindow();
117
+ if (!window) return;
118
+ const decorView = window.getDecorView();
119
+ const controller = (decorView as any).getWindowInsetsController?.();
120
+ if (controller) {
121
+ const APPEARANCE_LIGHT_STATUS_BARS = 8; // WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
122
+ // Android "light status bar" = dark icons; invert from our naming
123
+ controller.setSystemBarsAppearance(
124
+ style === 'dark' ? APPEARANCE_LIGHT_STATUS_BARS : 0,
125
+ APPEARANCE_LIGHT_STATUS_BARS
126
+ );
127
+ }
128
+ }
129
+ }
@@ -0,0 +1,67 @@
1
+ import { Utils, isAndroid, isIOS } from '@nativescript/core';
2
+
3
+ /** Native toast (trimmed to what the kit exposes). */
4
+ export function showToast(message: string, duration: 'short' | 'long' = 'short'): void {
5
+ const ms = duration === 'long' ? 3500 : 2000;
6
+ if (isAndroid) {
7
+ const Toast = (android as any).widget.Toast;
8
+ Toast.makeText(
9
+ Utils.android.getApplicationContext(),
10
+ message,
11
+ duration === 'long' ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT
12
+ ).show();
13
+ } else if (isIOS) {
14
+ showIOSToast(message, ms);
15
+ }
16
+ }
17
+
18
+ function showIOSToast(message: string, durationMs: number): void {
19
+ const rootVC = Utils.ios.getRootViewController();
20
+ if (!rootVC?.view) return;
21
+
22
+ const containerView = UIView.alloc().initWithFrame(CGRectMake(0, 0, 300, 60));
23
+ containerView.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0.08, 0.08, 0.1, 0.6);
24
+ containerView.layer.cornerRadius = 12;
25
+ containerView.clipsToBounds = true;
26
+
27
+ const blurEffect = UIBlurEffect.effectWithStyle(UIBlurEffectStyle.Dark);
28
+ const blurView = UIVisualEffectView.alloc().initWithEffect(blurEffect);
29
+ blurView.frame = containerView.bounds;
30
+ blurView.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
31
+ containerView.addSubview(blurView);
32
+
33
+ const label = UILabel.alloc().initWithFrame(CGRectMake(16, 0, 268, 60));
34
+ label.text = message;
35
+ label.textColor = UIColor.whiteColor;
36
+ label.textAlignment = NSTextAlignment.Center;
37
+ label.font = UIFont.systemFontOfSizeWeight(15, UIFontWeightMedium);
38
+ label.numberOfLines = 2;
39
+ blurView.contentView.addSubview(label);
40
+
41
+ const screenBounds = UIScreen.mainScreen.bounds;
42
+ containerView.center = CGPointMake(screenBounds.size.width / 2, screenBounds.size.height - 120);
43
+
44
+ rootVC.view.addSubview(containerView);
45
+ containerView.alpha = 0;
46
+ containerView.transform = CGAffineTransformMakeScale(0.8, 0.8);
47
+
48
+ UIView.animateWithDurationDelayOptionsAnimationsCompletion(
49
+ 0.3, 0, UIViewAnimationOptions.CurveEaseOut,
50
+ () => {
51
+ containerView.alpha = 1;
52
+ containerView.transform = CGAffineTransformIdentity;
53
+ },
54
+ () => {
55
+ setTimeout(() => {
56
+ UIView.animateWithDurationDelayOptionsAnimationsCompletion(
57
+ 0.3, 0, UIViewAnimationOptions.CurveEaseIn,
58
+ () => {
59
+ containerView.alpha = 0;
60
+ containerView.transform = CGAffineTransformMakeScale(0.8, 0.8);
61
+ },
62
+ () => containerView.removeFromSuperview()
63
+ );
64
+ }, durationMs);
65
+ }
66
+ );
67
+ }