@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.
- package/LICENSE +21 -0
- package/README.md +20 -0
- package/package.json +46 -0
- package/runtime/App_Resources/Android/app.gradle +34 -0
- package/runtime/App_Resources/Android/before-plugins.gradle +15 -0
- package/runtime/App_Resources/Android/src/main/AndroidManifest.xml +76 -0
- package/runtime/App_Resources/Android/src/main/res/drawable/ic_launcher_foreground.xml +15 -0
- package/runtime/App_Resources/Android/src/main/res/drawable-hdpi/background.png +0 -0
- package/runtime/App_Resources/Android/src/main/res/drawable-hdpi/logo.png +0 -0
- package/runtime/App_Resources/Android/src/main/res/drawable-ldpi/background.png +0 -0
- package/runtime/App_Resources/Android/src/main/res/drawable-ldpi/logo.png +0 -0
- package/runtime/App_Resources/Android/src/main/res/drawable-mdpi/background.png +0 -0
- package/runtime/App_Resources/Android/src/main/res/drawable-mdpi/logo.png +0 -0
- package/runtime/App_Resources/Android/src/main/res/drawable-nodpi/splash_screen.xml +8 -0
- package/runtime/App_Resources/Android/src/main/res/drawable-xhdpi/background.png +0 -0
- package/runtime/App_Resources/Android/src/main/res/drawable-xhdpi/logo.png +0 -0
- package/runtime/App_Resources/Android/src/main/res/drawable-xxhdpi/background.png +0 -0
- package/runtime/App_Resources/Android/src/main/res/drawable-xxhdpi/logo.png +0 -0
- package/runtime/App_Resources/Android/src/main/res/drawable-xxxhdpi/background.png +0 -0
- package/runtime/App_Resources/Android/src/main/res/drawable-xxxhdpi/logo.png +0 -0
- package/runtime/App_Resources/Android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/runtime/App_Resources/Android/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/runtime/App_Resources/Android/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/runtime/App_Resources/Android/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/runtime/App_Resources/Android/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/runtime/App_Resources/Android/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/runtime/App_Resources/Android/src/main/res/values/colors.xml +14 -0
- package/runtime/App_Resources/Android/src/main/res/values/ic_launcher_background.xml +4 -0
- package/runtime/App_Resources/Android/src/main/res/values/styles.xml +42 -0
- package/runtime/App_Resources/Android/src/main/res/values-v21/colors.xml +5 -0
- package/runtime/App_Resources/Android/src/main/res/values-v21/styles.xml +32 -0
- package/runtime/App_Resources/Android/src/main/res/values-v29/styles.xml +18 -0
- package/runtime/App_Resources/Android/src/main/res/xml/file_paths.xml +5 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-1024.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-20.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/Contents.json +6 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/Contents.json +23 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill@2x.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill@3x.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/Contents.json +23 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center@2x.png +0 -0
- package/runtime/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center@3x.png +0 -0
- package/runtime/App_Resources/iOS/Info.plist +60 -0
- package/runtime/App_Resources/iOS/LaunchScreen.storyboard +53 -0
- package/runtime/App_Resources/iOS/Podfile +13 -0
- package/runtime/App_Resources/iOS/PrivacyInfo.xcprivacy +59 -0
- package/runtime/App_Resources/iOS/build.xcconfig +7 -0
- package/runtime/app/app.ts +58 -0
- package/runtime/app/main-page.ts +116 -0
- package/runtime/app/main-page.xml +10 -0
- package/runtime/app/shell/active-modules.generated.ts +9 -0
- package/runtime/app/shell/android-helpers.ts +76 -0
- package/runtime/app/shell/bridge.ts +99 -0
- package/runtime/app/shell/capabilities.manifest.ts +207 -0
- package/runtime/app/shell/config.ts +35 -0
- package/runtime/app/shell/custom-webview.android.ts +270 -0
- package/runtime/app/shell/custom-webview.ios.ts +258 -0
- package/runtime/app/shell/custom-webview.ts +17 -0
- package/runtime/app/shell/env.ts +81 -0
- package/runtime/app/shell/events.ts +100 -0
- package/runtime/app/shell/fcm-bootstrap.generated.ts +1 -0
- package/runtime/app/shell/fcm-firebase.d.ts +16 -0
- package/runtime/app/shell/fcm-service.android.ts +40 -0
- package/runtime/app/shell/fcm-service.ios.ts +3 -0
- package/runtime/app/shell/handlers-android.ts +417 -0
- package/runtime/app/shell/handlers-billing.ts +197 -0
- package/runtime/app/shell/handlers-extended.ts +405 -0
- package/runtime/app/shell/handlers-health.ts +134 -0
- package/runtime/app/shell/handlers-media.ts +126 -0
- package/runtime/app/shell/handlers-oauth.ts +69 -0
- package/runtime/app/shell/handlers-parity.ts +257 -0
- package/runtime/app/shell/handlers-push.ts +189 -0
- package/runtime/app/shell/handlers-system.ts +70 -0
- package/runtime/app/shell/handlers.ts +149 -0
- package/runtime/app/shell/ios-frameworks.d.ts +36 -0
- package/runtime/app/shell/ios-image.ts +15 -0
- package/runtime/app/shell/ios-ui-delegate.ts +131 -0
- package/runtime/app/shell/mime.ts +29 -0
- package/runtime/app/shell/native-log.ts +22 -0
- package/runtime/app/shell/optional-handlers.generated.ts +10 -0
- package/runtime/app/shell/orientation.ts +43 -0
- package/runtime/app/shell/status-bar.ts +129 -0
- package/runtime/app/shell/toast.ts +67 -0
- package/runtime/app/shell/web-quirks.ts +131 -0
- package/runtime/modules-native/health/App_Resources/Android/src/main/java/cc/livx/appwrap/HealthConnectBridge.kt +74 -0
- package/runtime/nativescript.config.ts +15 -0
- package/runtime/package.json +21 -0
- package/runtime/references.d.ts +2 -0
- package/runtime/tsconfig.json +19 -0
- package/runtime/webpack.config.js +27 -0
- package/scripts/stage-assets.mjs +21 -0
- package/src/cli.ts +1328 -0
- package/templates/ci/fastlane/Fastfile +43 -0
- package/templates/ci/fastlane/Matchfile +3 -0
- package/templates/ci/github/workflows/appwrap-pr.yml +28 -0
- package/templates/ci/github/workflows/appwrap-release-android.yml +51 -0
- package/templates/ci/github/workflows/appwrap-release-ios.yml +41 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability manifest — the SINGLE, self-contained declaration of every appwrap capability.
|
|
3
|
+
*
|
|
4
|
+
* PURE DATA — no NativeScript globals — so the build-time CLI (bun) imports it to compose the shell,
|
|
5
|
+
* and the runtime imports it to build the handshake capability map. Each entry fully describes one
|
|
6
|
+
* capability's needs (permissions, background modes, native deps); the shell COLLECTS the union
|
|
7
|
+
* across the active set and dedups (two modules needing camera → one perm stamped once).
|
|
8
|
+
*
|
|
9
|
+
* Tiers:
|
|
10
|
+
* - `core: true` → always compiled in; cheap, no permissions (haptics, storage, toast, …).
|
|
11
|
+
* - opt-in (default) → only active when listed in `appwrap.json.modules` (explicit mode). These
|
|
12
|
+
* carry permissions / native deps / weight, so they're stripped from a build that doesn't ask
|
|
13
|
+
* for them — no bundled handler advertisement, no permission prompt, no native dependency.
|
|
14
|
+
*
|
|
15
|
+
* Back-compat: when `appwrap.json` has NO `modules` key, the CLI treats EVERY capability as active
|
|
16
|
+
* (today's behavior) and permissions still come solely from `appwrap.json.permissions{}`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** A native permission + a default human usage string (Apple requires app-facing copy).
|
|
20
|
+
* `appwrap.json.permissions{<domain>}` overrides the copy per-app; the key here is the iOS Info.plist
|
|
21
|
+
* key. Android perms need no usage string. */
|
|
22
|
+
export interface IosPermission {
|
|
23
|
+
/** Info.plist key, e.g. 'NSCameraUsageDescription'. */
|
|
24
|
+
key: string;
|
|
25
|
+
/** Override-key into `appwrap.json.permissions{}` (the legacy domain name). */
|
|
26
|
+
domain: string;
|
|
27
|
+
/** Default usage string if the app doesn't override it. */
|
|
28
|
+
defaultUsage: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ModuleManifest {
|
|
32
|
+
/** Capability name (also the handshake key + `kit.<name>` where 1:1). */
|
|
33
|
+
name: string;
|
|
34
|
+
/** Always-on, cheap, permission-free. Omitted/false = opt-in (gated by appwrap.json.modules). */
|
|
35
|
+
core?: boolean;
|
|
36
|
+
/** Handshake capability keys this module provides. Value 'native', or per-platform gating. */
|
|
37
|
+
capabilities: Record<string, 'native' | { ios?: boolean; android?: boolean }>;
|
|
38
|
+
/** The registration group this capability belongs to. Core caps share legacy groups (bundled
|
|
39
|
+
* together); opt-in NEW modules name their own group → the CLI imports it only when active, so it
|
|
40
|
+
* tree-shakes out of a build that doesn't use it. */
|
|
41
|
+
group: string;
|
|
42
|
+
ios?: {
|
|
43
|
+
permissions?: IosPermission[];
|
|
44
|
+
/** app.entitlements key/values (e.g. `{ 'com.apple.developer.healthkit': true }`). The CLI merges
|
|
45
|
+
* these (across active modules + push) into one App_Resources/iOS/app.entitlements. Values may be
|
|
46
|
+
* boolean, string, or string[]. */
|
|
47
|
+
entitlements?: Record<string, boolean | string | string[]>;
|
|
48
|
+
};
|
|
49
|
+
android?: {
|
|
50
|
+
/** AndroidManifest <uses-permission> names. */
|
|
51
|
+
permissions?: string[];
|
|
52
|
+
/** app.gradle dependency coordinates, e.g. 'androidx.health.connect:connect-client:1.1.0-rc01'. */
|
|
53
|
+
gradleDeps?: string[];
|
|
54
|
+
/** Module ships Kotlin native source → the CLI enables Kotlin in the NS Android build. */
|
|
55
|
+
kotlin?: boolean;
|
|
56
|
+
/** Raw XML injected inside the AndroidManifest `<application>` (activities, providers,
|
|
57
|
+
* intent-filters a capability needs — e.g. Health Connect's permission-rationale activity). */
|
|
58
|
+
manifestApplication?: string;
|
|
59
|
+
};
|
|
60
|
+
/** Module-owned native source: a dir under `runtime/modules-native/<nativeSrc>/` mirroring the
|
|
61
|
+
* App_Resources layout, copied into `native/` ONLY when the module is active (stays strippable).
|
|
62
|
+
* Defaults to the module `name` when the convention dir exists. */
|
|
63
|
+
nativeSrc?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const MODULES: ModuleManifest[] = [
|
|
67
|
+
// ── core (always on, no permissions) ───────────────────────────────────
|
|
68
|
+
{ name: 'haptics', core: true, group: 'core', capabilities: { haptics: 'native' } },
|
|
69
|
+
{ name: 'share', core: true, group: 'core', capabilities: { share: 'native', shareFiles: 'native' } },
|
|
70
|
+
{ name: 'storage', core: true, group: 'core', capabilities: { storage: 'native', secureStorage: 'native' } },
|
|
71
|
+
{ name: 'toast', core: true, group: 'core', capabilities: { toast: 'native' } },
|
|
72
|
+
{ name: 'statusBar', core: true, group: 'core', capabilities: { statusBar: 'native', themeColor: 'native' } },
|
|
73
|
+
{ name: 'device', core: true, group: 'extended', capabilities: { device: 'native' } },
|
|
74
|
+
{ name: 'clipboard', core: true, group: 'extended', capabilities: { clipboard: 'native' } },
|
|
75
|
+
{ name: 'network', core: true, group: 'extended', capabilities: { network: 'native' } },
|
|
76
|
+
{ name: 'screen', core: true, group: 'extended', capabilities: { screen: 'native', dialogs: 'native', orientation: 'native' } },
|
|
77
|
+
{ name: 'app', core: true, group: 'system', capabilities: { app: 'native', browser: 'native' } },
|
|
78
|
+
|
|
79
|
+
// ── opt-in: permission / dependency / weight-bearing ───────────────────
|
|
80
|
+
// POST_NOTIFICATIONS / VIBRATE / USE_BIOMETRIC etc. are in the template's baseline manifest already.
|
|
81
|
+
{ name: 'notifications', group: 'extended', capabilities: { notifications: 'native' } },
|
|
82
|
+
{
|
|
83
|
+
name: 'biometrics', group: 'extended',
|
|
84
|
+
capabilities: { biometrics: 'native' },
|
|
85
|
+
ios: { permissions: [{ key: 'NSFaceIDUsageDescription', domain: 'faceid', defaultUsage: 'Authenticate with Face ID.' }] },
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'geo', group: 'extended',
|
|
89
|
+
capabilities: { geo: 'native' },
|
|
90
|
+
ios: { permissions: [{ key: 'NSLocationWhenInUseUsageDescription', domain: 'location', defaultUsage: 'Show your location.' }] },
|
|
91
|
+
android: { permissions: ['android.permission.ACCESS_FINE_LOCATION', 'android.permission.ACCESS_COARSE_LOCATION'] },
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'photos', group: 'extended',
|
|
95
|
+
capabilities: { photos: 'native' },
|
|
96
|
+
ios: { permissions: [{ key: 'NSPhotoLibraryUsageDescription', domain: 'photos', defaultUsage: 'Pick a photo from your library.' }] },
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'camera', group: 'media',
|
|
100
|
+
capabilities: { camera: 'native' },
|
|
101
|
+
ios: { permissions: [{ key: 'NSCameraUsageDescription', domain: 'camera', defaultUsage: 'Capture a photo.' }] },
|
|
102
|
+
android: { permissions: ['android.permission.CAMERA'] },
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'media', group: 'media',
|
|
106
|
+
capabilities: { media: 'native' },
|
|
107
|
+
ios: { permissions: [
|
|
108
|
+
{ key: 'NSCameraUsageDescription', domain: 'camera', defaultUsage: 'Use the camera.' },
|
|
109
|
+
{ key: 'NSMicrophoneUsageDescription', domain: 'microphone', defaultUsage: 'Use the microphone.' },
|
|
110
|
+
] },
|
|
111
|
+
android: { permissions: ['android.permission.CAMERA', 'android.permission.RECORD_AUDIO'] },
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'motion', group: 'parity',
|
|
115
|
+
capabilities: { motion: 'native' },
|
|
116
|
+
ios: { permissions: [{ key: 'NSMotionUsageDescription', domain: 'motion', defaultUsage: 'Read device motion sensors.' }] },
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'contacts', group: 'parity',
|
|
120
|
+
capabilities: { contacts: 'native' },
|
|
121
|
+
android: { permissions: ['android.permission.READ_CONTACTS'] },
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'calendar', group: 'parity',
|
|
125
|
+
capabilities: { calendar: 'native' },
|
|
126
|
+
ios: { permissions: [
|
|
127
|
+
{ key: 'NSCalendarsFullAccessUsageDescription', domain: 'calendar', defaultUsage: 'Add events to your calendar.' },
|
|
128
|
+
{ key: 'NSCalendarsUsageDescription', domain: 'calendar', defaultUsage: 'Add events to your calendar.' },
|
|
129
|
+
] },
|
|
130
|
+
android: { permissions: ['android.permission.READ_CALENDAR', 'android.permission.WRITE_CALENDAR'] },
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'reviews', group: 'system',
|
|
134
|
+
capabilities: { reviews: { ios: true, android: false } },
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'billing', group: 'billing',
|
|
138
|
+
capabilities: { billing: { ios: true, android: false } },
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// ── oauth — system-browser OAuth (iOS ASWebAuthenticationSession) ──
|
|
142
|
+
// Opt-in, strippable (own handler file). No permissions/entitlements — ASWebAuthenticationSession
|
|
143
|
+
// needs none; it only relies on the app's urlScheme so the provider redirect returns to the app.
|
|
144
|
+
// Lets Google et al. complete sign-in they reject inside an embedded WebView (disallowed_useragent).
|
|
145
|
+
// Android (Custom Tabs + intent callback) is a future addition.
|
|
146
|
+
{
|
|
147
|
+
name: 'oauth', group: 'oauth',
|
|
148
|
+
capabilities: { oauth: { ios: true, android: false } },
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
// ── health (steps) — opt-in heavy module; FG live + BG via OS step store ──
|
|
152
|
+
{
|
|
153
|
+
name: 'health', group: 'health',
|
|
154
|
+
capabilities: { health: 'native' },
|
|
155
|
+
// iOS reads HealthKit (the Health app's aggregated total incl. Apple Watch): health-share usage
|
|
156
|
+
// string + the healthkit entitlement, both module-owned. No background execution — HealthKit
|
|
157
|
+
// already has the steps the OS recorded while the app was killed.
|
|
158
|
+
ios: {
|
|
159
|
+
permissions: [{ key: 'NSHealthShareUsageDescription', domain: 'health', defaultUsage: 'Read your step count from the Health app.' }],
|
|
160
|
+
entitlements: { 'com.apple.developer.healthkit': true },
|
|
161
|
+
},
|
|
162
|
+
// Android primary = Health Connect (system store, Wear-inclusive, survives kill — matches iOS).
|
|
163
|
+
// Its client is Kotlin-coroutine-only, so the module ships HealthConnectBridge.kt (nativeSrc) and
|
|
164
|
+
// flags kotlin:true. Falls back to SensorManager TYPE_STEP_COUNTER (ACTIVITY_RECOGNITION) when HC absent.
|
|
165
|
+
android: {
|
|
166
|
+
permissions: ['android.permission.health.READ_STEPS', 'android.permission.ACTIVITY_RECOGNITION'],
|
|
167
|
+
gradleDeps: [
|
|
168
|
+
'androidx.health.connect:connect-client:1.1.0-rc01',
|
|
169
|
+
'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0',
|
|
170
|
+
],
|
|
171
|
+
kotlin: true,
|
|
172
|
+
// Health Connect refuses to honor permissions unless the app declares a rationale activity
|
|
173
|
+
// handling VIEW_PERMISSION_USAGE / HEALTH_PERMISSIONS (alias onto the NS main activity).
|
|
174
|
+
manifestApplication: `<activity-alias
|
|
175
|
+
android:name="ViewPermissionUsageActivity"
|
|
176
|
+
android:exported="true"
|
|
177
|
+
android:targetActivity="com.tns.NativeScriptActivity"
|
|
178
|
+
android:permission="android.permission.START_VIEW_PERMISSION_USAGE">
|
|
179
|
+
<intent-filter>
|
|
180
|
+
<action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
|
|
181
|
+
<category android:name="android.intent.category.HEALTH_PERMISSIONS" />
|
|
182
|
+
</intent-filter>
|
|
183
|
+
</activity-alias>`,
|
|
184
|
+
},
|
|
185
|
+
nativeSrc: 'health',
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
/** Opt-in registration groups that own their own NS handler file (strippable when inactive). Core
|
|
190
|
+
* groups (core/extended/parity/system/media/billing) are always bundled; only these are CLI-gated. */
|
|
191
|
+
export const OPTIONAL_GROUPS = ['health', 'oauth'] as const;
|
|
192
|
+
|
|
193
|
+
/** Resolve the active capability map for the handshake from a set of active capability names. */
|
|
194
|
+
export function buildCapabilityMap(
|
|
195
|
+
activeNames: Set<string>,
|
|
196
|
+
platform: 'ios' | 'android'
|
|
197
|
+
): Record<string, 'native' | 'none'> {
|
|
198
|
+
const map: Record<string, 'native' | 'none'> = {};
|
|
199
|
+
for (const m of MODULES) {
|
|
200
|
+
if (!m.core && !activeNames.has(m.name)) continue;
|
|
201
|
+
for (const [cap, val] of Object.entries(m.capabilities)) {
|
|
202
|
+
if (val === 'native') map[cap] = 'native';
|
|
203
|
+
else map[cap] = val[platform] ? 'native' : 'none';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return map;
|
|
207
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell config — stamped by `appwrap init` from appwrap.json.
|
|
3
|
+
*/
|
|
4
|
+
export const SHELL_CONFIG = {
|
|
5
|
+
appId: 'cc.livx.hellowrap',
|
|
6
|
+
name: 'Hello AppWrap',
|
|
7
|
+
version: '0.1.0',
|
|
8
|
+
/** Entry file inside the bundled www/ folder. */
|
|
9
|
+
entry: 'index.html',
|
|
10
|
+
/** Page + status bar background while the WebView boots. */
|
|
11
|
+
backgroundColor: '#0b1020',
|
|
12
|
+
/** 'light' = white status bar icons. */
|
|
13
|
+
statusBarStyle: 'light' as 'light' | 'dark',
|
|
14
|
+
/** Android only (experimental). true = WebView draws edge-to-edge under transparent bars +
|
|
15
|
+
* safe-area insets injected as `--saie-*` CSS vars; false = bars show the page backgroundColor. */
|
|
16
|
+
edgeToEdge: false,
|
|
17
|
+
/** Loader: 'app' = app:// scheme handler (stable origin, ES modules, bundled www); 'file' = debug
|
|
18
|
+
* fallback; 'server' = load `serverUrl` live (dev HMR over LAN, or pointing at a deployed URL). */
|
|
19
|
+
loader: 'app' as 'app' | 'file' | 'server',
|
|
20
|
+
/** Live URL loaded when loader === 'server' (e.g. http://192.168.x.x:5173 dev, or a deployed https URL). */
|
|
21
|
+
serverUrl: '',
|
|
22
|
+
/** Absolute backend origin for an offline (app://) PWA whose API/WS calls were originally same-origin.
|
|
23
|
+
* Injected to the page as `window.__APPWRAP_BACKEND_ORIGIN__`; empty = same-origin (browser default). */
|
|
24
|
+
backendOrigin: '',
|
|
25
|
+
/** Debug/dev mode: keep the screen awake (no auto-lock while foreground) + WebView inspectable
|
|
26
|
+
* (Safari Web Inspector / chrome://inspect). Set true by `appwrap deploy`; never in store builds. */
|
|
27
|
+
debug: false,
|
|
28
|
+
/** Value written to `localStorage.DEBUG` in debug mode so the PWA logger goes verbose ('*' = all). */
|
|
29
|
+
debugLog: '*',
|
|
30
|
+
/** Remote push configured, per platform (iOS aps-environment entitlement / Android FCM). Drives the
|
|
31
|
+
* `push` capability flag at runtime by platform — off unless `appwrap.json.push` enables it, so an
|
|
32
|
+
* un-provisioned build honestly reports 'none' (and a personal-team iOS build keeps `pushIos:false`). */
|
|
33
|
+
pushIos: false,
|
|
34
|
+
pushAndroid: false,
|
|
35
|
+
};
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { WebView, Utils, knownFolders, path as nsPath, File } from '@nativescript/core';
|
|
2
|
+
import { SHELL_CONFIG } from './config';
|
|
3
|
+
import { mimeFor } from './mime';
|
|
4
|
+
import { APPWRAP_GLOBALS_JS, NATIVE_FEEL_JS } from './web-quirks';
|
|
5
|
+
import { envGlobalsJs } from './env';
|
|
6
|
+
import { requestPermissions } from './android-helpers';
|
|
7
|
+
|
|
8
|
+
declare const android: any, java: any, androidx: any;
|
|
9
|
+
|
|
10
|
+
/** Stable in-app origin — secure context, ES modules work (file:// blocks them). */
|
|
11
|
+
export const APP_ORIGIN = 'https://appwrap.local';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Web→native transport shim. postMessage tunnels through window.prompt(), which
|
|
15
|
+
* the shell intercepts synchronously in WebChromeClient.onJsPrompt — no compiled
|
|
16
|
+
* @JavascriptInterface class, no polling. Injected at document start (androidx.webkit)
|
|
17
|
+
* with an onPageStarted fallback; the guard makes double-injection a no-op.
|
|
18
|
+
*/
|
|
19
|
+
const TRANSPORT_SHIM = `(function(){
|
|
20
|
+
if (window.appwrapNative) return;
|
|
21
|
+
window.appwrapNative = { postMessage: function(json){ window.prompt('__appwrap__:' + json); } };
|
|
22
|
+
})();`;
|
|
23
|
+
|
|
24
|
+
const PROMPT_PREFIX = '__appwrap__:';
|
|
25
|
+
|
|
26
|
+
/** Document-start scripts: env hints + framework globals (backend origin) + bridge transport +
|
|
27
|
+
* native-feel. Globals first so the page can read __APPWRAP__ / __APPWRAP_BACKEND_ORIGIN__ before its
|
|
28
|
+
* own scripts run. Built lazily (not a const) so envGlobalsJs() detects against a live activity context. */
|
|
29
|
+
function buildBootstrapJs(): string {
|
|
30
|
+
return `${envGlobalsJs()}\n${APPWRAP_GLOBALS_JS}\n${TRANSPORT_SHIM}\n${NATIVE_FEEL_JS}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Android CustomWebView — replaces NS's default clients after creation:
|
|
35
|
+
* WebChromeClient.onJsPrompt is the appwrap transport, WebViewClient
|
|
36
|
+
* intercepts https://appwrap.local/* and serves the bundled PWA from www/
|
|
37
|
+
* (mirror of the iOS app:// WKURLSchemeHandler).
|
|
38
|
+
*/
|
|
39
|
+
export class CustomWebView extends WebView {
|
|
40
|
+
/** Set by the bridge before load; receives raw envelope JSON. */
|
|
41
|
+
onAppwrapMessage: ((json: string) => void) | null = null;
|
|
42
|
+
|
|
43
|
+
initNativeView(): void {
|
|
44
|
+
super.initNativeView();
|
|
45
|
+
const wv = this.android;
|
|
46
|
+
if (!wv) return;
|
|
47
|
+
|
|
48
|
+
// Debug mode only: debuggable via chrome://inspect (full console + network) + keep the screen
|
|
49
|
+
// awake while foreground for continuous troubleshooting. Off in store builds.
|
|
50
|
+
if (SHELL_CONFIG.debug) {
|
|
51
|
+
try { android.webkit.WebView.setWebContentsDebuggingEnabled(true); } catch (e) { /* older API */ }
|
|
52
|
+
try { wv.setKeepScreenOn(true); } catch (e) { /* no-op */ }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const settings = wv.getSettings();
|
|
56
|
+
settings.setJavaScriptEnabled(true);
|
|
57
|
+
settings.setDomStorageEnabled(true);
|
|
58
|
+
settings.setAllowFileAccess(true);
|
|
59
|
+
settings.setMediaPlaybackRequiresUserGesture(false); // speaker: autoplay allowed
|
|
60
|
+
// Native feel: no pinch-zoom controls.
|
|
61
|
+
settings.setSupportZoom(false);
|
|
62
|
+
settings.setBuiltInZoomControls(false);
|
|
63
|
+
settings.setDisplayZoomControls(false);
|
|
64
|
+
// No rubber-band overscroll glow.
|
|
65
|
+
wv.setOverScrollMode(android.view.View.OVER_SCROLL_NEVER);
|
|
66
|
+
|
|
67
|
+
this.installDocumentStartShim(wv);
|
|
68
|
+
wv.setWebViewClient(createAssetServingClient());
|
|
69
|
+
wv.setWebChromeClient(this.createTransportChromeClient());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Stop/restart the WebView's render + JS-timer pipeline when the app is backgrounded/foregrounded
|
|
74
|
+
* (wired to Application suspend/resume in main-page). Android does NOT reliably halt rAF/CSS
|
|
75
|
+
* animations when the activity isn't visible, so a page running a continuous animation keeps the
|
|
76
|
+
* compositor — and the CPU — busy in the background. onPause()/pauseTimers() stops that; the latter
|
|
77
|
+
* is process-wide but harmless with our single WebView. iOS suspends rAF on its own (no-op there).
|
|
78
|
+
*/
|
|
79
|
+
setRenderingActive(active: boolean): void {
|
|
80
|
+
const wv = this.android;
|
|
81
|
+
if (!wv) return;
|
|
82
|
+
try {
|
|
83
|
+
if (active) { wv.onResume(); wv.resumeTimers(); }
|
|
84
|
+
else { wv.onPause(); wv.pauseTimers(); }
|
|
85
|
+
} catch (e) {
|
|
86
|
+
console.warn('AppWrap: setRenderingActive failed', e);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private installDocumentStartShim(wv: any): void {
|
|
91
|
+
try {
|
|
92
|
+
const wkt = androidx.webkit;
|
|
93
|
+
if (wkt.WebViewFeature.isFeatureSupported(wkt.WebViewFeature.DOCUMENT_START_SCRIPT)) {
|
|
94
|
+
const rules = new java.util.HashSet();
|
|
95
|
+
rules.add(APP_ORIGIN);
|
|
96
|
+
wkt.WebViewCompat.addDocumentStartJavaScript(wv, buildBootstrapJs(), rules);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
console.warn('AppWrap: DOCUMENT_START_SCRIPT unsupported — relying on onPageStarted shim');
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.warn('AppWrap: addDocumentStartJavaScript failed', e);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private createTransportChromeClient(): any {
|
|
106
|
+
const owner = new WeakRef<CustomWebView>(this);
|
|
107
|
+
const ChromeClient = (android.webkit.WebChromeClient as any).extend({
|
|
108
|
+
onJsPrompt(_view: any, _url: string, message: string, _defaultValue: string, result: any): boolean {
|
|
109
|
+
if (typeof message === 'string' && message.startsWith(PROMPT_PREFIX)) {
|
|
110
|
+
result.confirm('');
|
|
111
|
+
const view = owner.get ? owner.get() : (owner as any).deref();
|
|
112
|
+
view?.onAppwrapMessage?.(message.slice(PROMPT_PREFIX.length));
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return false; // genuine page prompt — default handling
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// getUserMedia: grant the WebView's per-origin capture after ensuring the
|
|
119
|
+
// app holds the matching OS runtime permission (CAMERA / RECORD_AUDIO).
|
|
120
|
+
onPermissionRequest(request: any): void {
|
|
121
|
+
const PR = android.webkit.PermissionRequest;
|
|
122
|
+
const resources: string[] = Array.from(request.getResources());
|
|
123
|
+
const perms = new Set<string>();
|
|
124
|
+
for (const r of resources) {
|
|
125
|
+
if (r === PR.RESOURCE_VIDEO_CAPTURE) perms.add('android.permission.CAMERA');
|
|
126
|
+
if (r === PR.RESOURCE_AUDIO_CAPTURE) perms.add('android.permission.RECORD_AUDIO');
|
|
127
|
+
}
|
|
128
|
+
if (!perms.size) {
|
|
129
|
+
Utils.dispatchToMainThread(() => request.grant(request.getResources()));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
requestPermissions(Array.from(perms)).then((ok) =>
|
|
133
|
+
Utils.dispatchToMainThread(() => (ok ? request.grant(request.getResources()) : request.deny()))
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
return new ChromeClient();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** https://appwrap.local/<path> → file under www/; SPA fallback to the entry for extension-less misses. */
|
|
142
|
+
function createAssetServingClient(): any {
|
|
143
|
+
const wwwPath = nsPath.join(knownFolders.currentApp().path, 'www');
|
|
144
|
+
|
|
145
|
+
const respond = (filePath: string, ext: string) => {
|
|
146
|
+
const stream = new java.io.FileInputStream(filePath);
|
|
147
|
+
return new android.webkit.WebResourceResponse(mimeFor(ext, filePath), 'utf-8', stream);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const emptyStream = () => new java.io.ByteArrayInputStream((Array as any).create('byte', 0));
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* PROXY a non-local path to the backend (mirror of the iOS scheme-handler proxy) so a
|
|
154
|
+
* same-origin PWA's relative /api, /functions and vendor (/_vendor/sdk.js) requests resolve
|
|
155
|
+
* under app://appwrap.local — no CORS, same-origin.
|
|
156
|
+
*
|
|
157
|
+
* THREADING: NativeScript marshals the shouldInterceptRequest JS callback onto the JS/main thread,
|
|
158
|
+
* so a synchronous HttpURLConnection here throws NetworkOnMainThreadException. Instead we return
|
|
159
|
+
* immediately with a PipedInputStream and do the network read on a background java.lang.Thread that
|
|
160
|
+
* pumps the body into the pipe — the WebView drains it lazily. mime/charset are best-effort from the
|
|
161
|
+
* path extension (we can't read upstream headers without first blocking).
|
|
162
|
+
*
|
|
163
|
+
* LIMITATION: Android's shouldInterceptRequest exposes no request body, so only body-less methods
|
|
164
|
+
* (GET/HEAD) are proxied — POST/PUT/PATCH return 501 rather than silently sending an empty body.
|
|
165
|
+
* (BodDB writes ride the WS, not HTTP POST, so this is rarely hit.)
|
|
166
|
+
*/
|
|
167
|
+
const proxyToBackend = (rel: string, query: string, ext: string, method: string, headers: any): any => {
|
|
168
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
169
|
+
return new android.webkit.WebResourceResponse(
|
|
170
|
+
'text/plain', 'utf-8', 501, 'Not Implemented', new java.util.HashMap(),
|
|
171
|
+
new java.io.ByteArrayInputStream(new java.lang.String(
|
|
172
|
+
'appwrap Android shell cannot proxy request bodies (shouldInterceptRequest limitation)'
|
|
173
|
+
).getBytes('UTF-8'))
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const pin = new java.io.PipedInputStream(1 << 16);
|
|
177
|
+
const pout = new java.io.PipedOutputStream(pin);
|
|
178
|
+
const target = `${SHELL_CONFIG.backendOrigin.replace(/\/+$/, '')}/${rel}${query}`;
|
|
179
|
+
const pump = new java.lang.Runnable({
|
|
180
|
+
run() {
|
|
181
|
+
let is: any = null;
|
|
182
|
+
try {
|
|
183
|
+
const conn = new java.net.URL(target).openConnection();
|
|
184
|
+
conn.setRequestMethod(method);
|
|
185
|
+
conn.setInstanceFollowRedirects(true);
|
|
186
|
+
conn.setConnectTimeout(15000);
|
|
187
|
+
conn.setReadTimeout(20000);
|
|
188
|
+
if (headers) {
|
|
189
|
+
const it = headers.entrySet().iterator();
|
|
190
|
+
while (it.hasNext()) {
|
|
191
|
+
const e = it.next();
|
|
192
|
+
const k = String(e.getKey());
|
|
193
|
+
if (k.toLowerCase() === 'host') continue; // let URLConnection set Host from the target
|
|
194
|
+
conn.setRequestProperty(k, String(e.getValue()));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
conn.connect();
|
|
198
|
+
const status = conn.getResponseCode();
|
|
199
|
+
is = status >= 400 ? conn.getErrorStream() : conn.getInputStream();
|
|
200
|
+
const buf = (Array as any).create('byte', 16384);
|
|
201
|
+
let n: number;
|
|
202
|
+
while (is && (n = is.read(buf)) !== -1) pout.write(buf, 0, n);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
console.log('[appwrap] proxy failed for /' + rel + ': ' + e);
|
|
205
|
+
} finally {
|
|
206
|
+
try { if (is) is.close(); } catch (e) { /* no-op */ }
|
|
207
|
+
try { pout.close(); } catch (e) { /* no-op */ }
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
new java.lang.Thread(pump).start();
|
|
212
|
+
const outHeaders = new java.util.HashMap();
|
|
213
|
+
outHeaders.put('Access-Control-Allow-Origin', '*');
|
|
214
|
+
outHeaders.put('Cache-Control', 'no-cache');
|
|
215
|
+
// mime/charset best-effort from the path; status 200 (real status is unknown until the bg read).
|
|
216
|
+
return new android.webkit.WebResourceResponse(mimeFor(ext, rel), 'utf-8', 200, 'OK', outHeaders, pin);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const Client = (android.webkit.WebViewClient as any).extend({
|
|
220
|
+
shouldInterceptRequest(_view: any, request: any): any {
|
|
221
|
+
const isObj = typeof request !== 'string';
|
|
222
|
+
const url: string = isObj ? String(request.getUrl().toString()) : request;
|
|
223
|
+
if (!url.startsWith(APP_ORIGIN)) return null; // external — let WebView handle it
|
|
224
|
+
|
|
225
|
+
const tail = url.slice(APP_ORIGIN.length);
|
|
226
|
+
const qIdx = tail.search(/[?#]/);
|
|
227
|
+
const query = qIdx >= 0 && tail[qIdx] === '?' ? tail.slice(qIdx).split('#')[0] : '';
|
|
228
|
+
let rel = decodeURIComponent(tail.split(/[?#]/)[0]).replace(/^\/+/, '');
|
|
229
|
+
if (!rel) rel = SHELL_CONFIG.entry;
|
|
230
|
+
let filePath = nsPath.join(wwwPath, rel);
|
|
231
|
+
const ext = rel.includes('.') ? rel.split('.').pop()!.toLowerCase() : '';
|
|
232
|
+
|
|
233
|
+
if (File.exists(filePath)) return respond(filePath, ext);
|
|
234
|
+
|
|
235
|
+
if (!ext) {
|
|
236
|
+
// extension-less navigation → SPA fallback
|
|
237
|
+
return respond(nsPath.join(wwwPath, SHELL_CONFIG.entry), 'html');
|
|
238
|
+
}
|
|
239
|
+
// Not a local asset → proxy to the backend (mirror of iOS), making relative
|
|
240
|
+
// backend/vendor requests same-origin under app://appwrap.local.
|
|
241
|
+
if (SHELL_CONFIG.backendOrigin) {
|
|
242
|
+
const method = (isObj && request.getMethod && String(request.getMethod())) || 'GET';
|
|
243
|
+
const headers = isObj && request.getRequestHeaders ? request.getRequestHeaders() : null;
|
|
244
|
+
return proxyToBackend(rel, query, ext, method.toUpperCase(), headers);
|
|
245
|
+
}
|
|
246
|
+
return new android.webkit.WebResourceResponse(
|
|
247
|
+
'text/plain', 'utf-8', 404, 'Not Found', new java.util.HashMap(), emptyStream()
|
|
248
|
+
);
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
onPageStarted(view: any, _url: string, _favicon: any): void {
|
|
252
|
+
// Fallback injection — no-op when the document-start scripts already ran
|
|
253
|
+
view.evaluateJavascript(buildBootstrapJs(), null);
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
// `appwrap dev` points at a LAN dev server that almost always uses a self-signed / mkcert TLS
|
|
257
|
+
// cert the device's trust store doesn't know — the WebView would otherwise hard-fail with
|
|
258
|
+
// ERR_CERT_AUTHORITY_INVALID. Proceed past it ONLY in a debug build AND only when actually in
|
|
259
|
+
// server-loader (dev) mode. Production app:// builds never reach this (local assets, no TLS).
|
|
260
|
+
onReceivedSslError(_view: any, handler: any, error: any): void {
|
|
261
|
+
if (SHELL_CONFIG.debug && SHELL_CONFIG.loader === 'server') {
|
|
262
|
+
console.warn('AppWrap: proceeding past dev-server SSL error (debug+server only):', String(error));
|
|
263
|
+
handler.proceed();
|
|
264
|
+
} else {
|
|
265
|
+
handler.cancel();
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
return new Client();
|
|
270
|
+
}
|