@livx.cc/appwrap 0.39.5 → 0.39.9
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/Android/src/main/res/values-v29/styles.xml +4 -0
- package/runtime/app/shell/handlers-android.ts +13 -6
- package/runtime/app/shell/handlers-apple-signin.ts +75 -63
- package/runtime/app/shell/handlers-billing.ts +84 -72
- package/runtime/app/shell/handlers-extended.ts +117 -103
- package/runtime/app/shell/handlers-fs.ts +41 -31
- package/runtime/app/shell/handlers-oauth.ts +22 -12
- package/runtime/app/shell/handlers-parity.ts +81 -68
- package/runtime/app/shell/handlers-scanner.ts +26 -16
- package/runtime/app/shell/handlers-speech.ts +20 -17
- package/runtime/app/shell/handlers-system.ts +18 -8
- package/runtime/app/shell/ios-ui-delegate.ts +117 -106
- package/runtime/app/shell/status-bar.ts +40 -8
- package/src/cli.ts +73 -11
- package/templates/ci/fastlane/Fastfile +14 -0
package/package.json
CHANGED
|
@@ -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
|
-
//
|
|
276
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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:
|
|
12
|
-
let activeProvider:
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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:
|
|
64
|
-
let observer:
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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():
|
|
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
|
|