@livx.cc/appwrap 0.20.2 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livx.cc/appwrap",
3
- "version": "0.20.2",
3
+ "version": "0.20.3",
4
4
  "description": "Wrap any PWA into a native app with native capabilities (appwrap runtime + @livx.cc/native-kit).",
5
5
  "license": "MIT",
6
6
  "author": "Elya Livshitz",
@@ -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 and no data types are collected BY THE SHELL. If your PWA collects user
12
- data (account, analytics, location) or uses tracking, add the matching NSPrivacyCollectedDataTypes /
13
- NSPrivacyTracking entries here — see tracking-analytics + store-readiness.
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');