@livx.cc/appwrap 0.20.1 → 0.20.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/App_Resources/iOS/PrivacyInfo.xcprivacy +33 -4
- 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/templates/ci/github/workflows/appwrap-pr.yml +2 -1
- package/templates/ci/github/workflows/appwrap-release-android.yml +3 -2
- package/templates/ci/github/workflows/appwrap-release-ios.yml +3 -2
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>
|
|
@@ -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');
|
|
@@ -22,7 +22,8 @@ jobs:
|
|
|
22
22
|
- uses: actions/setup-node@v4
|
|
23
23
|
with: { node-version: 22 }
|
|
24
24
|
- run: bun install && bun run build.ts
|
|
25
|
-
|
|
25
|
+
# Pin global tools — unpinned installs can grab a breaking release mid-flight. Bump deliberately.
|
|
26
|
+
- run: npm i -g nativescript@9.0.6
|
|
26
27
|
- run: bunx @livx.cc/appwrap init # native/ is generated (gitignored) — regenerate it fresh in CI
|
|
27
28
|
- working-directory: native
|
|
28
29
|
run: npm install && ns build ios --emulator
|
|
@@ -24,7 +24,8 @@ jobs:
|
|
|
24
24
|
with: { distribution: temurin, java-version: 17 }
|
|
25
25
|
- uses: android-actions/setup-android@v3
|
|
26
26
|
- run: bun install && bun run build.ts # → dist/ (the PWA bundle the wrapper ships)
|
|
27
|
-
|
|
27
|
+
# Pin global tools — unpinned installs can grab a breaking release mid-flight. Bump deliberately.
|
|
28
|
+
- run: npm i -g nativescript@9.0.6
|
|
28
29
|
- run: bunx @livx.cc/appwrap init # native/ is generated (gitignored) — regenerate fresh in CI
|
|
29
30
|
- working-directory: native
|
|
30
31
|
run: npm install
|
|
@@ -43,7 +44,7 @@ jobs:
|
|
|
43
44
|
path: native/platforms/android/app/build/outputs/bundle/release/app-release.aab
|
|
44
45
|
- uses: ruby/setup-ruby@v1
|
|
45
46
|
with: { ruby-version: '3.3', bundler-cache: false }
|
|
46
|
-
- run: gem install fastlane
|
|
47
|
+
- run: gem install fastlane -v 2.236.1
|
|
47
48
|
- name: Upload to Play (internal)
|
|
48
49
|
working-directory: native
|
|
49
50
|
env:
|
|
@@ -26,13 +26,14 @@ jobs:
|
|
|
26
26
|
- uses: actions/setup-node@v4
|
|
27
27
|
with: { node-version: 22 }
|
|
28
28
|
- run: bun install && bun run build.ts
|
|
29
|
-
|
|
29
|
+
# Pin global tools — unpinned installs can grab a breaking release mid-flight. Bump deliberately.
|
|
30
|
+
- run: npm i -g nativescript@9.0.6
|
|
30
31
|
- run: bunx @livx.cc/appwrap init # native/ is generated (gitignored) — regenerate it fresh in CI
|
|
31
32
|
- working-directory: native
|
|
32
33
|
run: npm install
|
|
33
34
|
- uses: ruby/setup-ruby@v1
|
|
34
35
|
with: { ruby-version: '3.3', bundler-cache: false }
|
|
35
|
-
- run: gem install fastlane
|
|
36
|
+
- run: gem install fastlane -v 2.236.1
|
|
36
37
|
- name: Set up match deploy key (read-only SSH on the certs repo)
|
|
37
38
|
run: |
|
|
38
39
|
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|