@livx.cc/appwrap 0.39.4 → 0.39.8

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.39.4",
3
+ "version": "0.39.8",
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",
@@ -10,6 +10,10 @@
10
10
  but the way the app is converted may lead to visual issues
11
11
  -->
12
12
  <item name="android:forceDarkAllowed">false</item>
13
+
14
+ <!-- Draw into the display cutout/notch region so the app can be genuinely edge-to-edge
15
+ (paired with runtime enableEdgeToEdge + the edgeToEdge config's WebView un-boxing). -->
16
+ <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
13
17
  </style>
14
18
 
15
19
  <style name="AppTheme" parent="AppThemeBase29">
@@ -272,8 +272,11 @@ export function registerAndroidHandlers(): void {
272
272
  onSensorChanged(event: android.hardware.SensorEvent) {
273
273
  const v = event.values;
274
274
  if (event.sensor.getType() === android.hardware.Sensor.TYPE_ACCELEROMETER) {
275
- // Android accelerometer already reports m/s² incl. gravity — kit contract matches.
276
- lastMotion.ax = v[0]; lastMotion.ay = v[1]; lastMotion.az = v[2];
275
+ // NEGATE to match the kit/iOS convention: iOS CoreMotion delivers the gravity-DIRECTION
276
+ // vector (rest az ≈ -9.81), whereas Android's accelerometer reports the REACTION force
277
+ // (rest az ≈ +9.81) — the exact negation. Without this, gravity-vector tilt is inverted
278
+ // on every axis vs iOS (confirmed on-device: steering reversed). m/s² units already match.
279
+ lastMotion.ax = -v[0]; lastMotion.ay = -v[1]; lastMotion.az = -v[2];
277
280
  } else {
278
281
  lastMotion.rx = v[0]; lastMotion.ry = v[1]; lastMotion.rz = v[2];
279
282
  }
@@ -283,10 +286,14 @@ export function registerAndroidHandlers(): void {
283
286
  bridge.emit('motion.data', { ...lastMotion });
284
287
  },
285
288
  });
286
- const RATE = hz > 30 ? android.hardware.SensorManager.SENSOR_DELAY_FASTEST
287
- : android.hardware.SensorManager.SENSOR_DELAY_GAME;
288
- sm.registerListener(motionListener, accel, RATE);
289
- if (gyro) sm.registerListener(motionListener, gyro, RATE);
289
+ // Register at the REQUESTED rate as an explicit sampling period (µs), NOT SENSOR_DELAY_FASTEST:
290
+ // on Android 12+ (API 31) any rate faster than 200 Hz — including FASTEST (0 µs) — throws
291
+ // SecurityException unless the app declares HIGH_SAMPLING_RATE_SENSORS. Our hz is capped at 60
292
+ // (≈16.7 ms), comfortably under 200 Hz, so a plain period needs no extra permission. Floor at
293
+ // 5 ms (200 Hz) to stay below that threshold even if the cap ever rises.
294
+ const periodUs = Math.max(5000, Math.round(1_000_000 / hz));
295
+ sm.registerListener(motionListener, accel, periodUs);
296
+ if (gyro) sm.registerListener(motionListener, gyro, periodUs);
290
297
  });
291
298
 
292
299
  bridge.register('motion.stop', () => {
@@ -8,22 +8,8 @@ const err = (code: string, message: string) => Object.assign(new Error(message),
8
8
  // provider the moment the JS locals fall out of scope, which silently cancels the flow mid-handshake
9
9
  // (same lesson as handlers-oauth.ts `activeSession`/`activeProvider`).
10
10
  let activeController: ASAuthorizationController | null = null;
11
- let activeDelegate: AppleSignInDelegate | null = null;
12
- let activeProvider: AppleSignInContextProvider | null = null;
13
-
14
- @NativeClass()
15
- class AppleSignInContextProvider extends NSObject implements ASAuthorizationControllerPresentationContextProviding {
16
- static ObjCProtocols = [ASAuthorizationControllerPresentationContextProviding];
17
- static new(): AppleSignInContextProvider {
18
- return <AppleSignInContextProvider>super.new();
19
- }
20
- presentationAnchorForAuthorizationController(_controller: ASAuthorizationController): UIWindow {
21
- return (
22
- Utils.ios.getRootViewController()?.view?.window ??
23
- UIApplication.sharedApplication.keyWindow
24
- );
25
- }
26
- }
11
+ let activeDelegate: any = null;
12
+ let activeProvider: any = null;
27
13
 
28
14
  /** Decode an Apple-returned NSData JWT/code blob into a UTF-8 string (identityToken/authorizationCode). */
29
15
  function dataToUtf8(data: NSData | null): string | undefined {
@@ -32,61 +18,86 @@ function dataToUtf8(data: NSData | null): string | undefined {
32
18
  return s ? String(s) : undefined;
33
19
  }
34
20
 
35
- @NativeClass()
36
- class AppleSignInDelegate extends NSObject implements ASAuthorizationControllerDelegate {
37
- static ObjCProtocols = [ASAuthorizationControllerDelegate];
38
- // Per-call settlement (set right after `.new()`). The shared `settle` clears the ARC refs + recovers
39
- // the WebView before resolving/rejecting, so it runs exactly once per flow.
40
- rawNonce = '';
41
- settle!: (fn: () => void) => void;
42
- resolve!: (v: unknown) => void;
43
- reject!: (e: unknown) => void;
44
- static new(): AppleSignInDelegate {
45
- return <AppleSignInDelegate>super.new();
46
- }
21
+ // iOS-only native delegates. Defined lazily inside an isIOS-gated factory (mirrors banner.ts) so the
22
+ // shared module can instantiate on Android — NSObject/AS* are iOS globals, and a top-level
23
+ // `extends NSObject` would evaluate at ES-module load and crash the Android shell.
24
+ // any: module-level holders for runtime-built ObjC subclasses; each class BODY stays fully typed.
25
+ let AppleSignInContextProvider: any, AppleSignInDelegate: any;
26
+ function ensureIosDelegates(): void {
27
+ if (!isIOS || AppleSignInDelegate) return;
47
28
 
48
- authorizationControllerDidCompleteWithAuthorization(
49
- _controller: ASAuthorizationController,
50
- authorization: ASAuthorization
51
- ): void {
52
- const cred = authorization.credential as ASAuthorizationAppleIDCredential;
53
- const identityToken = dataToUtf8(cred.identityToken ?? null);
54
- if (!identityToken) {
55
- this.settle(() => this.reject(err('NATIVE_ERROR', 'appleSignIn: Apple returned no identityToken')));
56
- return;
29
+ @NativeClass()
30
+ class AppleSignInContextProviderImpl extends NSObject implements ASAuthorizationControllerPresentationContextProviding {
31
+ static ObjCProtocols = [ASAuthorizationControllerPresentationContextProviding];
32
+ static new(): AppleSignInContextProviderImpl {
33
+ return <AppleSignInContextProviderImpl>super.new();
57
34
  }
58
- const authorizationCode = dataToUtf8(cred.authorizationCode ?? null);
59
-
60
- // First-authorization-only profile. Apple omits name/email on subsequent sign-ins per Apple ID.
61
- let name: { givenName?: string; familyName?: string; displayName?: string } | undefined;
62
- if (cred.fullName) {
63
- const givenName = cred.fullName.givenName || undefined;
64
- const familyName = cred.fullName.familyName || undefined;
65
- // OS-composed display name (locale-aware) — convenience; the consumer can also build its own.
66
- const composed = NSPersonNameComponentsFormatter.localizedStringFromPersonNameComponentsStyleOptions(
67
- cred.fullName,
68
- NSPersonNameComponentsFormatterStyle.Default,
69
- 0 as NSPersonNameComponentsFormatterOptions
35
+ presentationAnchorForAuthorizationController(_controller: ASAuthorizationController): UIWindow {
36
+ return (
37
+ Utils.ios.getRootViewController()?.view?.window ??
38
+ UIApplication.sharedApplication.keyWindow
70
39
  );
71
- const displayName = (composed && String(composed)) || undefined;
72
- if (givenName || familyName || displayName) name = { givenName, familyName, displayName };
73
40
  }
74
- const email = cred.email || undefined;
75
- const user = name || email ? { ...(name ? { name } : {}), ...(email ? { email } : {}) } : undefined;
76
-
77
- this.settle(() =>
78
- this.resolve({ identityToken, authorizationCode, nonce: this.rawNonce, ...(user ? { user } : {}) })
79
- );
80
41
  }
42
+ AppleSignInContextProvider = AppleSignInContextProviderImpl;
43
+
44
+ @NativeClass()
45
+ class AppleSignInDelegateImpl extends NSObject implements ASAuthorizationControllerDelegate {
46
+ static ObjCProtocols = [ASAuthorizationControllerDelegate];
47
+ // Per-call settlement (set right after `.new()`). The shared `settle` clears the ARC refs + recovers
48
+ // the WebView before resolving/rejecting, so it runs exactly once per flow.
49
+ rawNonce = '';
50
+ settle!: (fn: () => void) => void;
51
+ resolve!: (v: unknown) => void;
52
+ reject!: (e: unknown) => void;
53
+ static new(): AppleSignInDelegateImpl {
54
+ return <AppleSignInDelegateImpl>super.new();
55
+ }
56
+
57
+ authorizationControllerDidCompleteWithAuthorization(
58
+ _controller: ASAuthorizationController,
59
+ authorization: ASAuthorization
60
+ ): void {
61
+ const cred = authorization.credential as ASAuthorizationAppleIDCredential;
62
+ const identityToken = dataToUtf8(cred.identityToken ?? null);
63
+ if (!identityToken) {
64
+ this.settle(() => this.reject(err('NATIVE_ERROR', 'appleSignIn: Apple returned no identityToken')));
65
+ return;
66
+ }
67
+ const authorizationCode = dataToUtf8(cred.authorizationCode ?? null);
68
+
69
+ // First-authorization-only profile. Apple omits name/email on subsequent sign-ins per Apple ID.
70
+ let name: { givenName?: string; familyName?: string; displayName?: string } | undefined;
71
+ if (cred.fullName) {
72
+ const givenName = cred.fullName.givenName || undefined;
73
+ const familyName = cred.fullName.familyName || undefined;
74
+ // OS-composed display name (locale-aware) — convenience; the consumer can also build its own.
75
+ const composed = NSPersonNameComponentsFormatter.localizedStringFromPersonNameComponentsStyleOptions(
76
+ cred.fullName,
77
+ NSPersonNameComponentsFormatterStyle.Default,
78
+ 0 as NSPersonNameComponentsFormatterOptions
79
+ );
80
+ const displayName = (composed && String(composed)) || undefined;
81
+ if (givenName || familyName || displayName) name = { givenName, familyName, displayName };
82
+ }
83
+ const email = cred.email || undefined;
84
+ const user = name || email ? { ...(name ? { name } : {}), ...(email ? { email } : {}) } : undefined;
85
+
86
+ this.settle(() =>
87
+ this.resolve({ identityToken, authorizationCode, nonce: this.rawNonce, ...(user ? { user } : {}) })
88
+ );
89
+ }
81
90
 
82
- authorizationControllerDidCompleteWithError(_controller: ASAuthorizationController, error: NSError): void {
83
- // ASAuthorizationError.Canceled === 1001 (user dismissed the sheet) → resolve cancelled, never throw.
84
- if (error.code === ASAuthorizationError.Canceled) {
85
- this.settle(() => this.resolve({ cancelled: true }));
86
- return;
91
+ authorizationControllerDidCompleteWithError(_controller: ASAuthorizationController, error: NSError): void {
92
+ // ASAuthorizationError.Canceled === 1001 (user dismissed the sheet) → resolve cancelled, never throw.
93
+ if (error.code === ASAuthorizationError.Canceled) {
94
+ this.settle(() => this.resolve({ cancelled: true }));
95
+ return;
96
+ }
97
+ this.settle(() => this.reject(err('NATIVE_ERROR', error.localizedDescription ?? 'appleSignIn failed')));
87
98
  }
88
- this.settle(() => this.reject(err('NATIVE_ERROR', error.localizedDescription ?? 'appleSignIn failed')));
89
99
  }
100
+ AppleSignInDelegate = AppleSignInDelegateImpl;
90
101
  }
91
102
 
92
103
  /**
@@ -112,6 +123,7 @@ class AppleSignInDelegate extends NSObject implements ASAuthorizationControllerD
112
123
  */
113
124
  export function registerAppleSignInHandlers(): void {
114
125
  if (!isIOS) return;
126
+ ensureIosDelegates();
115
127
 
116
128
  bridge.register(
117
129
  'appleSignIn.signIn',
@@ -60,88 +60,99 @@ function receiptFor(productId: string, transactionId?: string) {
60
60
  // In-flight resolvers, keyed so the single queue observer can route callbacks.
61
61
  const purchaseWaiters = new Map<string, { resolve: (r: any) => void; reject: (e: any) => void }>();
62
62
  let restoreWaiter: { receipts: any[]; resolve: (r: any[]) => void; reject: (e: any) => void } | null = null;
63
- const productRequests = new Set<{ request: SKProductsRequest; delegate: ProductsDelegate }>(); // keep strong refs to in-flight requests + delegates
64
- let observer: PaymentObserver | null = null;
65
-
66
- // SKPaymentQueue transaction observer. State (purchaseWaiters/restoreWaiter) lives at module
67
- // scope, so the delegate needs no instance fields.
68
- @NativeClass()
69
- class PaymentObserver extends NSObject implements SKPaymentTransactionObserver {
70
- static ObjCProtocols = [SKPaymentTransactionObserver];
71
- static new(): PaymentObserver {
72
- return <PaymentObserver>super.new();
73
- }
74
- paymentQueueUpdatedTransactions(queue: SKPaymentQueue, transactions: NSArray<SKPaymentTransaction>): void {
75
- const n = transactions.count;
76
- for (let i = 0; i < n; i++) {
77
- const t = transactions.objectAtIndex(i);
78
- const pid = String(t.payment.productIdentifier);
79
- switch (t.transactionState) {
80
- case STATE.Purchased: {
81
- queue.finishTransaction(t);
82
- const r = receiptFor(pid, String(t.transactionIdentifier ?? ''));
83
- bridge.emit('billing.transaction', r);
84
- purchaseWaiters.get(pid)?.resolve(r);
85
- purchaseWaiters.delete(pid);
86
- break;
87
- }
88
- case STATE.Restored: {
89
- queue.finishTransaction(t);
90
- const r = receiptFor(pid, String(t.transactionIdentifier ?? ''));
91
- if (restoreWaiter) restoreWaiter.receipts.push(r);
92
- bridge.emit('billing.transaction', r);
93
- break;
94
- }
95
- case STATE.Failed: {
96
- queue.finishTransaction(t);
97
- const cancelled = t.error && t.error.code === SK_ERR_CANCELLED;
98
- const e = err(cancelled ? 'DENIED' : 'NATIVE_ERROR', t.error?.localizedDescription ?? 'Purchase failed');
99
- purchaseWaiters.get(pid)?.reject(e);
100
- purchaseWaiters.delete(pid);
101
- break;
63
+ const productRequests = new Set<{ request: SKProductsRequest; delegate: any }>(); // keep strong refs to in-flight requests + delegates
64
+ let observer: any = null;
65
+
66
+ // iOS-only StoreKit delegates. Defined lazily inside an isIOS-gated factory (mirrors banner.ts) so the
67
+ // shared module can instantiate on Android — NSObject/SK* are iOS globals, and a top-level
68
+ // `extends NSObject` would evaluate at ES-module load and crash the Android shell.
69
+ // any: module-level holders for runtime-built ObjC subclasses; each class BODY stays fully typed.
70
+ let PaymentObserver: any, ProductsDelegate: any;
71
+ function ensureIosDelegates(): void {
72
+ if (!isIOS || PaymentObserver) return;
73
+
74
+ // SKPaymentQueue transaction observer. State (purchaseWaiters/restoreWaiter) lives at module
75
+ // scope, so the delegate needs no instance fields.
76
+ @NativeClass()
77
+ class PaymentObserverImpl extends NSObject implements SKPaymentTransactionObserver {
78
+ static ObjCProtocols = [SKPaymentTransactionObserver];
79
+ static new(): PaymentObserverImpl {
80
+ return <PaymentObserverImpl>super.new();
81
+ }
82
+ paymentQueueUpdatedTransactions(queue: SKPaymentQueue, transactions: NSArray<SKPaymentTransaction>): void {
83
+ const n = transactions.count;
84
+ for (let i = 0; i < n; i++) {
85
+ const t = transactions.objectAtIndex(i);
86
+ const pid = String(t.payment.productIdentifier);
87
+ switch (t.transactionState) {
88
+ case STATE.Purchased: {
89
+ queue.finishTransaction(t);
90
+ const r = receiptFor(pid, String(t.transactionIdentifier ?? ''));
91
+ bridge.emit('billing.transaction', r);
92
+ purchaseWaiters.get(pid)?.resolve(r);
93
+ purchaseWaiters.delete(pid);
94
+ break;
95
+ }
96
+ case STATE.Restored: {
97
+ queue.finishTransaction(t);
98
+ const r = receiptFor(pid, String(t.transactionIdentifier ?? ''));
99
+ if (restoreWaiter) restoreWaiter.receipts.push(r);
100
+ bridge.emit('billing.transaction', r);
101
+ break;
102
+ }
103
+ case STATE.Failed: {
104
+ queue.finishTransaction(t);
105
+ const cancelled = t.error && t.error.code === SK_ERR_CANCELLED;
106
+ const e = err(cancelled ? 'DENIED' : 'NATIVE_ERROR', t.error?.localizedDescription ?? 'Purchase failed');
107
+ purchaseWaiters.get(pid)?.reject(e);
108
+ purchaseWaiters.delete(pid);
109
+ break;
110
+ }
111
+ // Purchasing / Deferred: nothing to do.
102
112
  }
103
- // Purchasing / Deferred: nothing to do.
104
113
  }
105
114
  }
115
+ paymentQueueRestoreCompletedTransactionsFinished(_queue: SKPaymentQueue): void {
116
+ restoreWaiter?.resolve(restoreWaiter.receipts);
117
+ restoreWaiter = null;
118
+ }
119
+ paymentQueueRestoreCompletedTransactionsFailedWithError(_queue: SKPaymentQueue, error: NSError): void {
120
+ restoreWaiter?.reject(err('NATIVE_ERROR', error?.localizedDescription ?? 'Restore failed'));
121
+ restoreWaiter = null;
122
+ }
106
123
  }
107
- paymentQueueRestoreCompletedTransactionsFinished(_queue: SKPaymentQueue): void {
108
- restoreWaiter?.resolve(restoreWaiter.receipts);
109
- restoreWaiter = null;
110
- }
111
- paymentQueueRestoreCompletedTransactionsFailedWithError(_queue: SKPaymentQueue, error: NSError): void {
112
- restoreWaiter?.reject(err('NATIVE_ERROR', error?.localizedDescription ?? 'Restore failed'));
113
- restoreWaiter = null;
124
+ PaymentObserver = PaymentObserverImpl;
125
+
126
+ // SKProductsRequest delegate. Closure-captured state (resolve/reject + the strong-ref holder)
127
+ // is held as instance fields, assigned after new().
128
+ @NativeClass()
129
+ class ProductsDelegateImpl extends NSObject implements SKProductsRequestDelegate {
130
+ static ObjCProtocols = [SKProductsRequestDelegate];
131
+ onResolve?: (products: SKProduct[]) => void;
132
+ onReject?: (e: Error) => void;
133
+ holder?: { request: SKProductsRequest; delegate: any };
134
+ static new(): ProductsDelegateImpl {
135
+ return <ProductsDelegateImpl>super.new();
136
+ }
137
+ productsRequestDidReceiveResponse(_request: SKProductsRequest, response: SKProductsResponse): void {
138
+ const out: SKProduct[] = [];
139
+ const products = response.products;
140
+ for (let i = 0; i < products.count; i++) out.push(products.objectAtIndex(i));
141
+ if (this.holder) productRequests.delete(this.holder);
142
+ this.onResolve?.(out);
143
+ }
144
+ requestDidFailWithError(_request: SKRequest, error: NSError): void {
145
+ if (this.holder) productRequests.delete(this.holder);
146
+ this.onReject?.(err('NATIVE_ERROR', error?.localizedDescription ?? 'Products request failed'));
147
+ }
114
148
  }
149
+ ProductsDelegate = ProductsDelegateImpl;
115
150
  }
116
151
 
117
- function buildObserver(): PaymentObserver {
152
+ function buildObserver(): any {
118
153
  return PaymentObserver.new();
119
154
  }
120
155
 
121
- // SKProductsRequest delegate. Closure-captured state (resolve/reject + the strong-ref holder)
122
- // is held as instance fields, assigned after new().
123
- @NativeClass()
124
- class ProductsDelegate extends NSObject implements SKProductsRequestDelegate {
125
- static ObjCProtocols = [SKProductsRequestDelegate];
126
- onResolve?: (products: SKProduct[]) => void;
127
- onReject?: (e: Error) => void;
128
- holder?: { request: SKProductsRequest; delegate: ProductsDelegate };
129
- static new(): ProductsDelegate {
130
- return <ProductsDelegate>super.new();
131
- }
132
- productsRequestDidReceiveResponse(_request: SKProductsRequest, response: SKProductsResponse): void {
133
- const out: SKProduct[] = [];
134
- const products = response.products;
135
- for (let i = 0; i < products.count; i++) out.push(products.objectAtIndex(i));
136
- if (this.holder) productRequests.delete(this.holder);
137
- this.onResolve?.(out);
138
- }
139
- requestDidFailWithError(_request: SKRequest, error: NSError): void {
140
- if (this.holder) productRequests.delete(this.holder);
141
- this.onReject?.(err('NATIVE_ERROR', error?.localizedDescription ?? 'Products request failed'));
142
- }
143
- }
144
-
145
156
  /** One SKProductsRequest → resolves the raw SKProduct objects (used by both products + purchase). */
146
157
  function requestSKProducts(ids: string[]): Promise<SKProduct[]> {
147
158
  return new Promise((resolve, reject) => {
@@ -161,6 +172,7 @@ function requestSKProducts(ids: string[]): Promise<SKProduct[]> {
161
172
  export function registerBillingHandlers(): void {
162
173
  if (!isIOS) return; // Android billing is a separate, not-yet-wired handler
163
174
 
175
+ ensureIosDelegates();
164
176
  observer = buildObserver();
165
177
  SKPaymentQueue.defaultQueue().addTransactionObserver(observer);
166
178