@livx.cc/appwrap 0.20.2 → 0.20.4
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/package.json +1 -1
- package/runtime/App_Resources/iOS/PrivacyInfo.xcprivacy +33 -4
- package/runtime/app/shell/capabilities.manifest.ts +8 -1
- package/runtime/app/shell/custom-webview.ios.ts +14 -0
- package/runtime/app/shell/handlers-push.ts +36 -0
- package/runtime/app/shell/handlers-system.ts +73 -2
- package/src/cli.ts +12 -1
- package/templates/ci/github/workflows/appwrap-release-android.yml +6 -0
- package/templates/ci/github/workflows/appwrap-release-ios.yml +6 -0
package/package.json
CHANGED
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
- FileTimestamp (C617.1) — NativeScript reads bundle/app file metadata at runtime
|
|
9
9
|
- SystemBootTime (35F9.1) — NativeScript timers/uptime
|
|
10
10
|
- DiskSpace (E174.1) — NativeScript file I/O
|
|
11
|
-
NSPrivacyTracking is false
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
NSPrivacyTracking is false (no IDFA, no cross-app/third-party linkage). The shell surfaces install
|
|
12
|
+
context for FIRST-PARTY analytics via kit.context():
|
|
13
|
+
- DeviceID (CRC0) — identifierForVendor (IDFV), app-scoped, reset on uninstall; NOT IDFA.
|
|
14
|
+
- ProductInteraction (385F.1) — usage/app-environment props (platform, version, install source).
|
|
15
|
+
Both are declared NOT linked to identity and NOT used for tracking. If your PWA collects MORE
|
|
16
|
+
(account, location, contacts) or DOES track (ads/3rd-party linkage), add the matching
|
|
17
|
+
NSPrivacyCollectedDataTypes / set NSPrivacyTracking=true here — see tracking-analytics + store-readiness.
|
|
14
18
|
-->
|
|
15
19
|
<plist version="1.0">
|
|
16
20
|
<dict>
|
|
@@ -19,7 +23,32 @@
|
|
|
19
23
|
<key>NSPrivacyTrackingDomains</key>
|
|
20
24
|
<array/>
|
|
21
25
|
<key>NSPrivacyCollectedDataTypes</key>
|
|
22
|
-
<array
|
|
26
|
+
<array>
|
|
27
|
+
<dict>
|
|
28
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
29
|
+
<string>NSPrivacyCollectedDataTypeDeviceID</string>
|
|
30
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
31
|
+
<false/>
|
|
32
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
33
|
+
<false/>
|
|
34
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
35
|
+
<array>
|
|
36
|
+
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
|
37
|
+
</array>
|
|
38
|
+
</dict>
|
|
39
|
+
<dict>
|
|
40
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
41
|
+
<string>NSPrivacyCollectedDataTypeProductInteraction</string>
|
|
42
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
43
|
+
<false/>
|
|
44
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
45
|
+
<false/>
|
|
46
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
47
|
+
<array>
|
|
48
|
+
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
|
49
|
+
</array>
|
|
50
|
+
</dict>
|
|
51
|
+
</array>
|
|
23
52
|
<key>NSPrivacyAccessedAPITypes</key>
|
|
24
53
|
<array>
|
|
25
54
|
<dict>
|
|
@@ -156,7 +156,14 @@ export const MODULES: ModuleManifest[] = [
|
|
|
156
156
|
// string + the healthkit entitlement, both module-owned. No background execution — HealthKit
|
|
157
157
|
// already has the steps the OS recorded while the app was killed.
|
|
158
158
|
ios: {
|
|
159
|
-
|
|
159
|
+
// App Store validation REQUIRES both Share AND Update purpose strings whenever the HealthKit
|
|
160
|
+
// entitlement is present — even for a read-only app (else upload fails 409 "Missing purpose
|
|
161
|
+
// string … NSHealthUpdateUsageDescription"). Device debug builds don't validate this; only the
|
|
162
|
+
// App Store upload does.
|
|
163
|
+
permissions: [
|
|
164
|
+
{ key: 'NSHealthShareUsageDescription', domain: 'health', defaultUsage: 'Read your step count from the Health app.' },
|
|
165
|
+
{ key: 'NSHealthUpdateUsageDescription', domain: 'health', defaultUsage: 'Read your step count from the Health app.' },
|
|
166
|
+
],
|
|
160
167
|
entitlements: { 'com.apple.developer.healthkit': true },
|
|
161
168
|
},
|
|
162
169
|
// Android primary = Health Connect (system store, Wear-inclusive, survives kill — matches iOS).
|
|
@@ -17,6 +17,14 @@ import { appwrapNativeLog } from './native-log';
|
|
|
17
17
|
// Forwarded WebView console/errors → the shared native-log sink (Documents/appwrap-web.log).
|
|
18
18
|
const appendWebLog = appwrapNativeLog;
|
|
19
19
|
|
|
20
|
+
/** Parse a #rrggbb config color into a UIColor (for the WebView background). */
|
|
21
|
+
function uiColorFromHex(hex?: string): any {
|
|
22
|
+
const m = /^#?([0-9a-fA-F]{6})$/.exec((hex ?? '').trim());
|
|
23
|
+
if (!m) return null;
|
|
24
|
+
const n = parseInt(m[1], 16);
|
|
25
|
+
return UIColor.colorWithRedGreenBlueAlpha(((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255, 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
export class CustomWebView extends WebView {
|
|
21
29
|
/** Set by the bridge before load; receives raw envelope JSON. */
|
|
22
30
|
onAppwrapMessage: ((json: string) => void) | null = null;
|
|
@@ -104,6 +112,12 @@ export class CustomWebView extends WebView {
|
|
|
104
112
|
// and the auto content-inset (the page owns safe areas via env()).
|
|
105
113
|
webView.scrollView.bounces = false;
|
|
106
114
|
webView.scrollView.contentInsetAdjustmentBehavior = 2; // .never
|
|
115
|
+
|
|
116
|
+
// No white flash before the page paints: make the WebView transparent so the (themed) NS page
|
|
117
|
+
// backgroundColor shows through during load — critical for loader:'server' (network) startups.
|
|
118
|
+
const bg = uiColorFromHex(SHELL_CONFIG.backgroundColor);
|
|
119
|
+
webView.opaque = false;
|
|
120
|
+
if (bg) { webView.backgroundColor = bg; webView.scrollView.backgroundColor = bg; }
|
|
107
121
|
webView.allowsLinkPreview = false;
|
|
108
122
|
// Edge-swipe to go back/forward in WebView history — the iOS native gesture
|
|
109
123
|
// users expect. SPAs that own their own history still get it for free.
|
|
@@ -93,6 +93,29 @@ export function registerPushHandlers(): void {
|
|
|
93
93
|
throw unsupported();
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
+
bridge.register('push.permissionStatus', () => {
|
|
97
|
+
if (isIOS) {
|
|
98
|
+
return new Promise<string>((resolve) => {
|
|
99
|
+
UNUserNotificationCenter.currentNotificationCenter().getNotificationSettingsWithCompletionHandler(
|
|
100
|
+
(settings) => {
|
|
101
|
+
switch (settings.authorizationStatus) {
|
|
102
|
+
case UNAuthorizationStatus.Authorized:
|
|
103
|
+
case UNAuthorizationStatus.Provisional:
|
|
104
|
+
case UNAuthorizationStatus.Ephemeral:
|
|
105
|
+
return resolve('granted');
|
|
106
|
+
case UNAuthorizationStatus.Denied:
|
|
107
|
+
return resolve('denied');
|
|
108
|
+
default:
|
|
109
|
+
return resolve('notDetermined');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (isAndroid) return androidPermissionStatus();
|
|
116
|
+
throw unsupported();
|
|
117
|
+
});
|
|
118
|
+
|
|
96
119
|
bridge.register('push.register', () => {
|
|
97
120
|
if (isIOS) {
|
|
98
121
|
return new Promise<PushToken>((resolve, reject) => {
|
|
@@ -164,6 +187,19 @@ function androidRequestPermission(): Promise<string> {
|
|
|
164
187
|
});
|
|
165
188
|
}
|
|
166
189
|
|
|
190
|
+
function androidPermissionStatus(): string {
|
|
191
|
+
// No "not-determined" concept on Android — report the effective on/off state.
|
|
192
|
+
// < API 33 has no runtime perm, but the user can still disable notifications in Settings.
|
|
193
|
+
try {
|
|
194
|
+
const ctx = Utils.android.getApplicationContext();
|
|
195
|
+
const enabled = androidx.core.app.NotificationManagerCompat.from(ctx).areNotificationsEnabled();
|
|
196
|
+
return enabled ? 'granted' : 'denied';
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.warn('AppWrap: push.permissionStatus failed', e);
|
|
199
|
+
return 'notDetermined';
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
167
203
|
function androidGetToken(): Promise<PushToken> {
|
|
168
204
|
return new Promise((resolve, reject) => {
|
|
169
205
|
try {
|
|
@@ -1,10 +1,79 @@
|
|
|
1
|
-
import { Application, Color, Utils, isAndroid, isIOS } from '@nativescript/core';
|
|
1
|
+
import { Application, ApplicationSettings, Color, Utils, isAndroid, isIOS } from '@nativescript/core';
|
|
2
2
|
import { bridge } from './bridge';
|
|
3
3
|
|
|
4
|
-
declare const android: any, androidx: any;
|
|
4
|
+
declare const android: any, androidx: any, java: any;
|
|
5
5
|
|
|
6
6
|
const err = (code: string, message: string) => Object.assign(new Error(message), { code });
|
|
7
7
|
|
|
8
|
+
/** Running on a simulator/emulator? (mirrors env.ts detection; kept local to avoid a cross-import). */
|
|
9
|
+
function detectEmulator(): boolean {
|
|
10
|
+
if (isIOS) {
|
|
11
|
+
return NSProcessInfo.processInfo.environment.objectForKey('SIMULATOR_DEVICE_NAME') != null;
|
|
12
|
+
}
|
|
13
|
+
const B = android.os.Build;
|
|
14
|
+
const fp = String(B.FINGERPRINT ?? '');
|
|
15
|
+
return (
|
|
16
|
+
fp.startsWith('generic') ||
|
|
17
|
+
fp.includes('vbox') ||
|
|
18
|
+
fp.includes('test-keys') ||
|
|
19
|
+
String(B.MODEL ?? '').includes('Emulator') ||
|
|
20
|
+
String(B.MANUFACTURER ?? '').includes('Genymotion') ||
|
|
21
|
+
String(B.HARDWARE ?? '').includes('goldfish') ||
|
|
22
|
+
String(B.HARDWARE ?? '').includes('ranchu')
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Install-level environment for analytics/identification. First-party & non-tracking:
|
|
28
|
+
* - iOS installId = identifierForVendor (NOT IDFA — no ATT prompt needed); source from the
|
|
29
|
+
* App Store receipt path (sandboxReceipt ⇒ TestFlight). iOS has no clean first/last-install API.
|
|
30
|
+
* - Android installId = a random UUID persisted in app storage (cleared on uninstall, like IDFV);
|
|
31
|
+
* source from the installer package name; first/last-install from PackageInfo.
|
|
32
|
+
*/
|
|
33
|
+
function appEnvironment() {
|
|
34
|
+
const isEmulator = detectEmulator();
|
|
35
|
+
if (isIOS) {
|
|
36
|
+
let source = 'sideload';
|
|
37
|
+
if (isEmulator) source = 'simulator';
|
|
38
|
+
// A dev/ad-hoc build carries embedded.mobileprovision; TestFlight & App Store builds do NOT.
|
|
39
|
+
// Both TestFlight AND dev builds have a `sandboxReceipt`, so the receipt name alone can't tell
|
|
40
|
+
// them apart — the provisioning profile is the reliable discriminator.
|
|
41
|
+
else if (NSBundle.mainBundle.pathForResourceOfType('embedded', 'mobileprovision')) source = 'sideload';
|
|
42
|
+
else {
|
|
43
|
+
const receipt = NSBundle.mainBundle.appStoreReceiptURL;
|
|
44
|
+
const name = receipt?.lastPathComponent ?? '';
|
|
45
|
+
if (name === 'sandboxReceipt') source = 'testflight';
|
|
46
|
+
else if (receipt && NSFileManager.defaultManager.fileExistsAtPath(receipt.path)) source = 'appstore';
|
|
47
|
+
}
|
|
48
|
+
const idfv = UIDevice.currentDevice.identifierForVendor;
|
|
49
|
+
return { source, installId: idfv ? idfv.UUIDString : undefined, isEmulator };
|
|
50
|
+
}
|
|
51
|
+
// Android
|
|
52
|
+
const ctx = Utils.android.getApplicationContext();
|
|
53
|
+
const pm = ctx.getPackageManager();
|
|
54
|
+
const pkg = ctx.getPackageName();
|
|
55
|
+
let source = isEmulator ? 'simulator' : 'sideload';
|
|
56
|
+
let firstInstallAt: number | undefined;
|
|
57
|
+
let lastUpdateAt: number | undefined;
|
|
58
|
+
try {
|
|
59
|
+
const installer = android.os.Build.VERSION.SDK_INT >= 30
|
|
60
|
+
? pm.getInstallSourceInfo(pkg)?.getInstallingPackageName()
|
|
61
|
+
: pm.getInstallerPackageName(pkg);
|
|
62
|
+
if (!isEmulator && (installer === 'com.android.vending' || installer === 'com.google.android.feedback')) source = 'playstore';
|
|
63
|
+
const pi = pm.getPackageInfo(pkg, 0);
|
|
64
|
+
firstInstallAt = Number(pi.firstInstallTime);
|
|
65
|
+
lastUpdateAt = Number(pi.lastUpdateTime);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.warn('AppWrap: app.environment install info failed', e);
|
|
68
|
+
}
|
|
69
|
+
let installId = ApplicationSettings.getString('kit:__installId', '');
|
|
70
|
+
if (!installId) {
|
|
71
|
+
installId = String(java.util.UUID.randomUUID().toString());
|
|
72
|
+
ApplicationSettings.setString('kit:__installId', installId);
|
|
73
|
+
}
|
|
74
|
+
return { source, installId, firstInstallAt, lastUpdateAt, isEmulator };
|
|
75
|
+
}
|
|
76
|
+
|
|
8
77
|
/**
|
|
9
78
|
* System / navigation handlers shared by both platforms (each method branches
|
|
10
79
|
* internally rather than relying on the iOS-first + Android-override split):
|
|
@@ -48,6 +117,8 @@ export function registerSystemHandlers(): void {
|
|
|
48
117
|
}
|
|
49
118
|
});
|
|
50
119
|
|
|
120
|
+
bridge.register('app.environment', () => appEnvironment());
|
|
121
|
+
|
|
51
122
|
bridge.register('browser.open', ({ url, toolbarColor }: { url: string; toolbarColor?: string }) => {
|
|
52
123
|
const target = String(url ?? '');
|
|
53
124
|
if (!target) throw err('NATIVE_ERROR', 'browser.open: empty url');
|
package/src/cli.ts
CHANGED
|
@@ -28,8 +28,19 @@ function deriveBuild(version: string): number {
|
|
|
28
28
|
return maj * 10000 + min * 100 + patch;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Resolved monotonic build number (iOS CFBundleVersion / Android versionCode).
|
|
33
|
+
* Precedence: `APPWRAP_BUILD_NUMBER` env (CI run #) > explicit `cfg.buildNumber` > derived from version.
|
|
34
|
+
* The env override means CI gets a monotonic, collision-free build number for free — no per-app config
|
|
35
|
+
* plumbing — which is the whole point: the derived default is CONSTANT per version, so repeat uploads
|
|
36
|
+
* of the same marketing version would 409 ("build already exists") without it.
|
|
37
|
+
*/
|
|
32
38
|
function buildNumberOf(cfg: AppwrapConfig): number {
|
|
39
|
+
const env = process.env.APPWRAP_BUILD_NUMBER;
|
|
40
|
+
if (env != null && env !== '') {
|
|
41
|
+
const n = parseInt(env, 10);
|
|
42
|
+
if (!Number.isNaN(n)) return n;
|
|
43
|
+
}
|
|
33
44
|
if (cfg.buildNumber != null) {
|
|
34
45
|
const n = parseInt(String(cfg.buildNumber), 10);
|
|
35
46
|
if (!Number.isNaN(n)) return n;
|
|
@@ -9,9 +9,15 @@
|
|
|
9
9
|
# exist on Play with one prior manual upload before supply works)
|
|
10
10
|
name: Release Android
|
|
11
11
|
on:
|
|
12
|
+
workflow_dispatch:
|
|
12
13
|
push:
|
|
13
14
|
tags: ['v*']
|
|
14
15
|
|
|
16
|
+
# Monotonic store build number (Android versionCode). appwrap CLI reads APPWRAP_BUILD_NUMBER over the
|
|
17
|
+
# constant derived default — Play rejects a versionCode it has already seen. run_number is increasing.
|
|
18
|
+
env:
|
|
19
|
+
APPWRAP_BUILD_NUMBER: ${{ github.run_number + 100 }}
|
|
20
|
+
|
|
15
21
|
jobs:
|
|
16
22
|
play:
|
|
17
23
|
runs-on: ubuntu-latest
|
|
@@ -14,6 +14,12 @@ on:
|
|
|
14
14
|
push:
|
|
15
15
|
tags: ['v*']
|
|
16
16
|
|
|
17
|
+
# Monotonic store build number (CFBundleVersion). The appwrap CLI reads APPWRAP_BUILD_NUMBER and it wins
|
|
18
|
+
# over the (constant-per-version) derived default — without this, repeat uploads of the same marketing
|
|
19
|
+
# version 409 ("build already exists"). run_number is strictly increasing; +100 clears low derived values.
|
|
20
|
+
env:
|
|
21
|
+
APPWRAP_BUILD_NUMBER: ${{ github.run_number + 100 }}
|
|
22
|
+
|
|
17
23
|
jobs:
|
|
18
24
|
testflight:
|
|
19
25
|
runs-on: macos-15
|