@livx.cc/appwrap 0.28.0 → 0.28.2
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/app.ts +55 -54
- package/runtime/app/shell/android-helpers.ts +6 -6
- package/runtime/app/shell/background-worker.android.ts +47 -0
- package/runtime/app/shell/background-worker.ios.ts +5 -0
- package/runtime/app/shell/background-worker.ts +7 -0
- package/runtime/app/shell/banner.ts +9 -4
- package/runtime/app/shell/bridge.ts +2 -0
- package/runtime/app/shell/custom-webview.android.ts +50 -23
- package/runtime/app/shell/custom-webview.ios.ts +90 -78
- package/runtime/app/shell/devmenu.ts +2 -2
- package/runtime/app/shell/env.ts +1 -3
- package/runtime/app/shell/events.ts +5 -4
- package/runtime/app/shell/handlers-android.ts +16 -12
- package/runtime/app/shell/handlers-background.ts +57 -78
- package/runtime/app/shell/handlers-billing.ts +90 -82
- package/runtime/app/shell/handlers-extended.ts +141 -110
- package/runtime/app/shell/handlers-fs.ts +53 -41
- package/runtime/app/shell/handlers-health.ts +27 -19
- package/runtime/app/shell/handlers-media.ts +8 -14
- package/runtime/app/shell/handlers-oauth.ts +19 -19
- package/runtime/app/shell/handlers-parity.ts +118 -89
- package/runtime/app/shell/handlers-push.ts +5 -3
- package/runtime/app/shell/handlers-reviews.ts +5 -6
- package/runtime/app/shell/handlers-scanner.ts +36 -38
- package/runtime/app/shell/handlers-speech.ts +43 -49
- package/runtime/app/shell/handlers-system.ts +1 -1
- package/runtime/app/shell/handlers.ts +4 -3
- package/runtime/app/shell/ios-frameworks.d.ts +9 -0
- package/runtime/app/shell/ios-ui-delegate.ts +112 -108
- package/runtime/app/shell/native-log.ts +1 -3
- package/runtime/app/shell/status-bar.ts +7 -4
- package/runtime/app/shell/toast.ts +1 -1
- package/runtime/references.d.ts +6 -0
- package/runtime/tsconfig.json +1 -0
- package/src/cli.ts +43 -18
- package/src/derive.ts +8 -0
- package/templates/ci/github/workflows/appwrap-pr.yml +3 -3
- package/templates/ci/github/workflows/appwrap-release-android.yml +2 -2
- package/templates/ci/github/workflows/appwrap-release-ios.yml +2 -2
|
@@ -9,16 +9,6 @@ import { bridge } from './bridge';
|
|
|
9
9
|
* separate handler (Play Billing) and is currently unimplemented (capability 'none').
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
declare const SKPaymentQueue: any;
|
|
13
|
-
declare const SKPayment: any;
|
|
14
|
-
declare const SKProductsRequest: any;
|
|
15
|
-
declare const SKProductsRequestDelegate: any;
|
|
16
|
-
declare const SKPaymentTransactionObserver: any;
|
|
17
|
-
declare const NSSet: any;
|
|
18
|
-
declare const NSBundle: any;
|
|
19
|
-
declare const NSData: any;
|
|
20
|
-
declare const NSNumberFormatter: any;
|
|
21
|
-
|
|
22
12
|
const PERIOD_UNIT = ['D', 'W', 'M', 'Y']; // SKProductPeriodUnit: Day/Week/Month/Year
|
|
23
13
|
const STATE = { Purchasing: 0, Purchased: 1, Failed: 2, Restored: 3, Deferred: 4 };
|
|
24
14
|
const SK_ERR_CANCELLED = 2; // SKErrorPaymentCancelled
|
|
@@ -35,10 +25,11 @@ function appReceipt(): string {
|
|
|
35
25
|
const url = NSBundle.mainBundle.appStoreReceiptURL;
|
|
36
26
|
if (!url) return '';
|
|
37
27
|
const data = NSData.dataWithContentsOfURL(url);
|
|
38
|
-
|
|
28
|
+
// 0 = NSDataBase64Encoding default (no options); enum-cast for the typed selector.
|
|
29
|
+
return data ? data.base64EncodedStringWithOptions(0 as unknown as NSDataBase64EncodingOptions) : '';
|
|
39
30
|
}
|
|
40
31
|
|
|
41
|
-
function mapProduct(p:
|
|
32
|
+
function mapProduct(p: SKProduct) {
|
|
42
33
|
const f = NSNumberFormatter.new();
|
|
43
34
|
f.numberStyle = 2; // CurrencyStyle
|
|
44
35
|
f.locale = p.priceLocale;
|
|
@@ -48,7 +39,8 @@ function mapProduct(p: any) {
|
|
|
48
39
|
title: String(p.localizedTitle ?? ''),
|
|
49
40
|
description: String(p.localizedDescription ?? ''),
|
|
50
41
|
price: p.price ? p.price.doubleValue : 0,
|
|
51
|
-
|
|
42
|
+
// stringFromNumber takes an NSNumber* at runtime; the .d.ts types it as number, so cast the NSDecimalNumber.
|
|
43
|
+
displayPrice: String(f.stringFromNumber(p.price as unknown as number) ?? ''),
|
|
52
44
|
currency: String(p.priceLocale?.currencyCode ?? ''),
|
|
53
45
|
type: sp ? 'autoRenewable' : 'unknown',
|
|
54
46
|
subscriptionPeriod: sp ? `P${sp.numberOfUnits}${PERIOD_UNIT[sp.unit] ?? 'D'}` : undefined,
|
|
@@ -62,82 +54,98 @@ function receiptFor(productId: string, transactionId?: string) {
|
|
|
62
54
|
// In-flight resolvers, keyed so the single queue observer can route callbacks.
|
|
63
55
|
const purchaseWaiters = new Map<string, { resolve: (r: any) => void; reject: (e: any) => void }>();
|
|
64
56
|
let restoreWaiter: { receipts: any[]; resolve: (r: any[]) => void; reject: (e: any) => void } | null = null;
|
|
65
|
-
const productRequests = new Set<
|
|
66
|
-
let observer:
|
|
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
|
-
purchaseWaiters.get(pid)?.reject(e);
|
|
97
|
-
purchaseWaiters.delete(pid);
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
// Purchasing / Deferred: nothing to do.
|
|
101
|
-
}
|
|
57
|
+
const productRequests = new Set<{ request: SKProductsRequest; delegate: ProductsDelegate }>(); // keep strong refs to in-flight requests + delegates
|
|
58
|
+
let observer: PaymentObserver | null = null;
|
|
59
|
+
|
|
60
|
+
// SKPaymentQueue transaction observer. State (purchaseWaiters/restoreWaiter) lives at module
|
|
61
|
+
// scope, so the delegate needs no instance fields.
|
|
62
|
+
@NativeClass()
|
|
63
|
+
class PaymentObserver extends NSObject implements SKPaymentTransactionObserver {
|
|
64
|
+
static ObjCProtocols = [SKPaymentTransactionObserver];
|
|
65
|
+
static new(): PaymentObserver {
|
|
66
|
+
return <PaymentObserver>super.new();
|
|
67
|
+
}
|
|
68
|
+
paymentQueueUpdatedTransactions(queue: SKPaymentQueue, transactions: NSArray<SKPaymentTransaction>): void {
|
|
69
|
+
const n = transactions.count;
|
|
70
|
+
for (let i = 0; i < n; i++) {
|
|
71
|
+
const t = transactions.objectAtIndex(i);
|
|
72
|
+
const pid = String(t.payment.productIdentifier);
|
|
73
|
+
switch (t.transactionState) {
|
|
74
|
+
case STATE.Purchased: {
|
|
75
|
+
queue.finishTransaction(t);
|
|
76
|
+
const r = receiptFor(pid, String(t.transactionIdentifier ?? ''));
|
|
77
|
+
bridge.emit('billing.transaction', r);
|
|
78
|
+
purchaseWaiters.get(pid)?.resolve(r);
|
|
79
|
+
purchaseWaiters.delete(pid);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case STATE.Restored: {
|
|
83
|
+
queue.finishTransaction(t);
|
|
84
|
+
const r = receiptFor(pid, String(t.transactionIdentifier ?? ''));
|
|
85
|
+
if (restoreWaiter) restoreWaiter.receipts.push(r);
|
|
86
|
+
bridge.emit('billing.transaction', r);
|
|
87
|
+
break;
|
|
102
88
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
89
|
+
case STATE.Failed: {
|
|
90
|
+
queue.finishTransaction(t);
|
|
91
|
+
const cancelled = t.error && t.error.code === SK_ERR_CANCELLED;
|
|
92
|
+
const e = err(cancelled ? 'DENIED' : 'NATIVE_ERROR', t.error?.localizedDescription ?? 'Purchase failed');
|
|
93
|
+
purchaseWaiters.get(pid)?.reject(e);
|
|
94
|
+
purchaseWaiters.delete(pid);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
// Purchasing / Deferred: nothing to do.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
paymentQueueRestoreCompletedTransactionsFinished(_queue: SKPaymentQueue): void {
|
|
102
|
+
restoreWaiter?.resolve(restoreWaiter.receipts);
|
|
103
|
+
restoreWaiter = null;
|
|
104
|
+
}
|
|
105
|
+
paymentQueueRestoreCompletedTransactionsFailedWithError(_queue: SKPaymentQueue, error: NSError): void {
|
|
106
|
+
restoreWaiter?.reject(err('NATIVE_ERROR', error?.localizedDescription ?? 'Restore failed'));
|
|
107
|
+
restoreWaiter = null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildObserver(): PaymentObserver {
|
|
112
|
+
return PaymentObserver.new();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// SKProductsRequest delegate. Closure-captured state (resolve/reject + the strong-ref holder)
|
|
116
|
+
// is held as instance fields, assigned after new().
|
|
117
|
+
@NativeClass()
|
|
118
|
+
class ProductsDelegate extends NSObject implements SKProductsRequestDelegate {
|
|
119
|
+
static ObjCProtocols = [SKProductsRequestDelegate];
|
|
120
|
+
onResolve?: (products: SKProduct[]) => void;
|
|
121
|
+
onReject?: (e: Error) => void;
|
|
122
|
+
holder?: { request: SKProductsRequest; delegate: ProductsDelegate };
|
|
123
|
+
static new(): ProductsDelegate {
|
|
124
|
+
return <ProductsDelegate>super.new();
|
|
125
|
+
}
|
|
126
|
+
productsRequestDidReceiveResponse(_request: SKProductsRequest, response: SKProductsResponse): void {
|
|
127
|
+
const out: SKProduct[] = [];
|
|
128
|
+
const products = response.products;
|
|
129
|
+
for (let i = 0; i < products.count; i++) out.push(products.objectAtIndex(i));
|
|
130
|
+
if (this.holder) productRequests.delete(this.holder);
|
|
131
|
+
this.onResolve?.(out);
|
|
132
|
+
}
|
|
133
|
+
requestDidFailWithError(_request: SKRequest, error: NSError): void {
|
|
134
|
+
if (this.holder) productRequests.delete(this.holder);
|
|
135
|
+
this.onReject?.(err('NATIVE_ERROR', error?.localizedDescription ?? 'Products request failed'));
|
|
136
|
+
}
|
|
116
137
|
}
|
|
117
138
|
|
|
118
139
|
/** One SKProductsRequest → resolves the raw SKProduct objects (used by both products + purchase). */
|
|
119
|
-
function requestSKProducts(ids: string[]): Promise<
|
|
140
|
+
function requestSKProducts(ids: string[]): Promise<SKProduct[]> {
|
|
120
141
|
return new Promise((resolve, reject) => {
|
|
121
|
-
const set = NSSet.setWithArray(ids
|
|
142
|
+
const set = NSSet.setWithArray<string>(ids);
|
|
122
143
|
const request = SKProductsRequest.alloc().initWithProductIdentifiers(set);
|
|
123
|
-
const
|
|
124
|
-
{
|
|
125
|
-
productsRequestDidReceiveResponse(_req: any, response: any): void {
|
|
126
|
-
const out: any[] = [];
|
|
127
|
-
const products = response.products;
|
|
128
|
-
for (let i = 0; i < products.count; i++) out.push(products.objectAtIndex(i));
|
|
129
|
-
productRequests.delete(holder);
|
|
130
|
-
resolve(out);
|
|
131
|
-
},
|
|
132
|
-
requestDidFailWithError(_req: any, error: any): void {
|
|
133
|
-
productRequests.delete(holder);
|
|
134
|
-
reject(err('NATIVE_ERROR', error?.localizedDescription ?? 'Products request failed'));
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
{ protocols: [SKProductsRequestDelegate] }
|
|
138
|
-
);
|
|
139
|
-
const delegate = DelegateClass.new();
|
|
144
|
+
const delegate = ProductsDelegate.new();
|
|
140
145
|
const holder = { request, delegate };
|
|
146
|
+
delegate.onResolve = resolve;
|
|
147
|
+
delegate.onReject = reject;
|
|
148
|
+
delegate.holder = holder;
|
|
141
149
|
productRequests.add(holder); // strong ref until the callback fires
|
|
142
150
|
request.delegate = delegate;
|
|
143
151
|
request.start();
|
|
@@ -5,6 +5,118 @@ import { onRemoteMessage } from './handlers-push';
|
|
|
5
5
|
import { uiImageToDataUrl } from './ios-image';
|
|
6
6
|
import { maskForLock, setIosOrientationMask } from './orientation';
|
|
7
7
|
|
|
8
|
+
interface GeoResult { lat: number; lng: number; accuracy: number; }
|
|
9
|
+
|
|
10
|
+
// CLLocationManager delegate. Closure-captured callbacks (resolve/reject/cleanup) are assigned as
|
|
11
|
+
// instance fields after new() — same pattern as handlers-speech/oauth.
|
|
12
|
+
@NativeClass()
|
|
13
|
+
class GeoDelegate extends NSObject implements CLLocationManagerDelegate {
|
|
14
|
+
static ObjCProtocols = [CLLocationManagerDelegate];
|
|
15
|
+
onResult?: (r: GeoResult) => void;
|
|
16
|
+
onError?: (e: Error) => void;
|
|
17
|
+
onCleanup?: () => void;
|
|
18
|
+
static new(): GeoDelegate {
|
|
19
|
+
return <GeoDelegate>super.new();
|
|
20
|
+
}
|
|
21
|
+
locationManagerDidUpdateLocations(_m: CLLocationManager, locations: NSArray<CLLocation>): void {
|
|
22
|
+
const loc = locations.lastObject;
|
|
23
|
+
if (!loc) return;
|
|
24
|
+
const result = {
|
|
25
|
+
lat: loc.coordinate.latitude,
|
|
26
|
+
lng: loc.coordinate.longitude,
|
|
27
|
+
accuracy: loc.horizontalAccuracy,
|
|
28
|
+
};
|
|
29
|
+
this.onCleanup?.();
|
|
30
|
+
this.onResult?.(result);
|
|
31
|
+
}
|
|
32
|
+
locationManagerDidFailWithError(_m: CLLocationManager, error: NSError): void {
|
|
33
|
+
this.onCleanup?.();
|
|
34
|
+
this.onError?.(Object.assign(new Error(error.localizedDescription), { code: 'DENIED' }));
|
|
35
|
+
}
|
|
36
|
+
locationManagerDidChangeAuthorization(m: CLLocationManager): void {
|
|
37
|
+
const st = m.authorizationStatus;
|
|
38
|
+
if (st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedWhenInUse ||
|
|
39
|
+
st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedAlways) {
|
|
40
|
+
m.startUpdatingLocation();
|
|
41
|
+
} else if (st === CLAuthorizationStatus.kCLAuthorizationStatusDenied) {
|
|
42
|
+
this.onCleanup?.();
|
|
43
|
+
this.onError?.(Object.assign(new Error('location permission denied'), { code: 'DENIED' }));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// PHPickerViewController delegate. Closure-captured opts (dataUrl/maxSize) + resolve assigned post-new().
|
|
49
|
+
@NativeClass()
|
|
50
|
+
class PhotoPickerDelegate extends NSObject implements PHPickerViewControllerDelegate {
|
|
51
|
+
static ObjCProtocols = [PHPickerViewControllerDelegate];
|
|
52
|
+
wantDataUrl?: boolean;
|
|
53
|
+
maxSize?: number;
|
|
54
|
+
onResult?: (r: { picked: boolean; width?: number; height?: number; dataUrl?: string }) => void;
|
|
55
|
+
static new(): PhotoPickerDelegate {
|
|
56
|
+
return <PhotoPickerDelegate>super.new();
|
|
57
|
+
}
|
|
58
|
+
pickerDidFinishPicking(picker: PHPickerViewController, results: NSArray<PHPickerResult>): void {
|
|
59
|
+
picker.dismissViewControllerAnimatedCompletion(true, null);
|
|
60
|
+
const result = results.count > 0 ? results.objectAtIndex(0) : null;
|
|
61
|
+
if (!result) return this.onResult?.({ picked: false });
|
|
62
|
+
result.itemProvider.loadObjectOfClassCompletionHandler(UIImage.class(), (img: UIImage) => {
|
|
63
|
+
if (!img) return this.onResult?.({ picked: true });
|
|
64
|
+
const out: { picked: boolean; width?: number; height?: number; dataUrl?: string } =
|
|
65
|
+
{ picked: true, width: img.size.width, height: img.size.height };
|
|
66
|
+
if (this.wantDataUrl) out.dataUrl = uiImageToDataUrl(img, this.maxSize ?? 1024);
|
|
67
|
+
this.onResult?.(out);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// UNUserNotificationCenter delegate — foreground present + tap routing. Idempotent singleton (notifDelegate).
|
|
73
|
+
@NativeClass()
|
|
74
|
+
class ForegroundNotificationDelegate extends NSObject implements UNUserNotificationCenterDelegate {
|
|
75
|
+
static ObjCProtocols = [UNUserNotificationCenterDelegate];
|
|
76
|
+
static new(): ForegroundNotificationDelegate {
|
|
77
|
+
return <ForegroundNotificationDelegate>super.new();
|
|
78
|
+
}
|
|
79
|
+
userNotificationCenterWillPresentNotificationWithCompletionHandler(
|
|
80
|
+
_center: UNUserNotificationCenter,
|
|
81
|
+
notification: UNNotification,
|
|
82
|
+
completionHandler: (opts: UNNotificationPresentationOptions) => void
|
|
83
|
+
): void {
|
|
84
|
+
// Foreground REMOTE push (push trigger) → surface to kit.push.onMessage.
|
|
85
|
+
if (isRemotePush(notification)) onRemoteMessage(notification.request.content.userInfo, false);
|
|
86
|
+
// Present banner + list + sound + badge even while foreground (min target iOS 16).
|
|
87
|
+
completionHandler(
|
|
88
|
+
UNNotificationPresentationOptions.Banner |
|
|
89
|
+
UNNotificationPresentationOptions.List |
|
|
90
|
+
UNNotificationPresentationOptions.Sound |
|
|
91
|
+
UNNotificationPresentationOptions.Badge
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
// Notification tapped → route its deep link through the same path as an external open.
|
|
95
|
+
userNotificationCenterDidReceiveNotificationResponseWithCompletionHandler(
|
|
96
|
+
_center: UNUserNotificationCenter,
|
|
97
|
+
response: UNNotificationResponse,
|
|
98
|
+
completionHandler: () => void
|
|
99
|
+
): void {
|
|
100
|
+
// Tapped REMOTE push → kit.push.onTap (with payload). Local notifs keep the deep-link path below.
|
|
101
|
+
if (isRemotePush(response.notification)) onRemoteMessage(response.notification.request.content.userInfo, true);
|
|
102
|
+
// userInfo may come back as an NSDictionary or an auto-marshalled JS object.
|
|
103
|
+
// any: dual-path payload (NSDictionary vs marshalled JS object) probed dynamically.
|
|
104
|
+
const info: any = response.notification.request.content.userInfo;
|
|
105
|
+
const url = info ? (typeof info.objectForKey === 'function' ? info.objectForKey('url') : info.url) : null;
|
|
106
|
+
// Diagnostic breadcrumb (persists across the cold relaunch) — surfaced in the handshake's debug field.
|
|
107
|
+
try {
|
|
108
|
+
ApplicationSettings.setString(
|
|
109
|
+
'kit:__notifTap',
|
|
110
|
+
JSON.stringify({ at: Date.now(), url: url ? String(url) : null, hadInfo: !!info })
|
|
111
|
+
);
|
|
112
|
+
} catch {
|
|
113
|
+
/* diagnostic only */
|
|
114
|
+
}
|
|
115
|
+
if (url) onDeepLink(String(url));
|
|
116
|
+
completionHandler();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
8
120
|
/**
|
|
9
121
|
* Extended capability handlers — iOS-first (Android arrives with the transport upgrade).
|
|
10
122
|
* Each handler is small and self-contained; heavy domains (push, files) come later.
|
|
@@ -62,11 +174,11 @@ export function registerExtendedHandlers(): void {
|
|
|
62
174
|
bridge.register('storage.secure.get', async ({ key }: { key: string }) =>
|
|
63
175
|
(await secure.get({ key: `kit:${key}` })) ?? null
|
|
64
176
|
);
|
|
65
|
-
bridge.register('storage.secure.set', ({ key, value }: { key: string; value: string }) =>
|
|
66
|
-
secure.set({ key: `kit:${key}`, value: String(value ?? '') }).then(() => undefined)
|
|
177
|
+
bridge.register('storage.secure.set', ({ key, value }: { key: string; value: string }): Promise<void> =>
|
|
178
|
+
secure.set({ key: `kit:${key}`, value: String(value ?? '') }).then((): void => undefined)
|
|
67
179
|
);
|
|
68
|
-
bridge.register('storage.secure.remove', ({ key }: { key: string }) =>
|
|
69
|
-
secure.remove({ key: `kit:${key}` }).then(() => undefined)
|
|
180
|
+
bridge.register('storage.secure.remove', ({ key }: { key: string }): Promise<void> =>
|
|
181
|
+
secure.remove({ key: `kit:${key}` }).then((): void => undefined)
|
|
70
182
|
);
|
|
71
183
|
|
|
72
184
|
// ── notifications (UNUserNotificationCenter, iOS) ──────────────────
|
|
@@ -85,7 +197,7 @@ export function registerExtendedHandlers(): void {
|
|
|
85
197
|
});
|
|
86
198
|
});
|
|
87
199
|
|
|
88
|
-
bridge.register('notifications.schedule', ({ id, title, body, delaySec, deepLink }:
|
|
200
|
+
bridge.register('notifications.schedule', ({ id, title, body, delaySec, deepLink }: { id?: number; title?: string; body?: string; delaySec?: number; deepLink?: string }) => {
|
|
89
201
|
if (!isIOS) throw Object.assign(new Error('iOS only for now'), { code: 'UNSUPPORTED' });
|
|
90
202
|
const nid = id ?? Math.floor(Math.random() * 100000);
|
|
91
203
|
return new Promise((resolve, reject) => {
|
|
@@ -95,6 +207,7 @@ export function registerExtendedHandlers(): void {
|
|
|
95
207
|
// Carry the deep-link on the notification; the tap delegate re-emits it.
|
|
96
208
|
// Plain JS object → NativeScript marshals it to NSDictionary (more reliable
|
|
97
209
|
// than dictionaryWithObjectForKey across NS versions).
|
|
210
|
+
// cast: NS marshals a plain JS object → NSDictionary at the interop boundary (typed NSDictionary).
|
|
98
211
|
if (deepLink) content.userInfo = { url: String(deepLink) } as any;
|
|
99
212
|
const trigger = UNTimeIntervalNotificationTrigger.triggerWithTimeIntervalRepeats(
|
|
100
213
|
Math.max(1, delaySec ?? 1),
|
|
@@ -130,7 +243,7 @@ export function registerExtendedHandlers(): void {
|
|
|
130
243
|
// iOS silently ignores the badge unless notification authorization (incl. .badge) was granted —
|
|
131
244
|
// the completion's error is the ONLY signal, so DON'T swallow it (else the badge just never
|
|
132
245
|
// appears with no explanation). Resolve regardless (the JS call itself didn't fail).
|
|
133
|
-
(error:
|
|
246
|
+
(error: NSError) => { if (error) console.warn('[appwrap] setBadge ignored — notifications not authorized?', String(error)); }
|
|
134
247
|
);
|
|
135
248
|
resolve();
|
|
136
249
|
});
|
|
@@ -152,8 +265,8 @@ export function registerExtendedHandlers(): void {
|
|
|
152
265
|
LAPolicy.DeviceOwnerAuthenticationWithBiometrics
|
|
153
266
|
);
|
|
154
267
|
const type =
|
|
155
|
-
ctx.biometryType === LABiometryType.
|
|
156
|
-
: ctx.biometryType === LABiometryType.
|
|
268
|
+
ctx.biometryType === LABiometryType.TypeFaceID ? 'face'
|
|
269
|
+
: ctx.biometryType === LABiometryType.TypeTouchID ? 'touch'
|
|
157
270
|
: 'none';
|
|
158
271
|
return { available, type };
|
|
159
272
|
});
|
|
@@ -183,7 +296,7 @@ export function registerExtendedHandlers(): void {
|
|
|
183
296
|
}, 15000);
|
|
184
297
|
|
|
185
298
|
let manager: CLLocationManager | null = null;
|
|
186
|
-
let delegate:
|
|
299
|
+
let delegate: GeoDelegate | null = null;
|
|
187
300
|
const cleanup = () => {
|
|
188
301
|
clearTimeout(timer);
|
|
189
302
|
manager?.stopUpdatingLocation();
|
|
@@ -191,40 +304,12 @@ export function registerExtendedHandlers(): void {
|
|
|
191
304
|
delegate = null;
|
|
192
305
|
};
|
|
193
306
|
|
|
194
|
-
const DelegateClass = (NSObject as any).extend(
|
|
195
|
-
{
|
|
196
|
-
locationManagerDidUpdateLocations(_m: CLLocationManager, locations: NSArray<CLLocation>) {
|
|
197
|
-
const loc = locations.lastObject;
|
|
198
|
-
if (!loc) return;
|
|
199
|
-
const result = {
|
|
200
|
-
lat: loc.coordinate.latitude,
|
|
201
|
-
lng: loc.coordinate.longitude,
|
|
202
|
-
accuracy: loc.horizontalAccuracy,
|
|
203
|
-
};
|
|
204
|
-
cleanup();
|
|
205
|
-
resolve(result);
|
|
206
|
-
},
|
|
207
|
-
locationManagerDidFailWithError(_m: CLLocationManager, error: NSError) {
|
|
208
|
-
cleanup();
|
|
209
|
-
reject(Object.assign(new Error(error.localizedDescription), { code: 'DENIED' }));
|
|
210
|
-
},
|
|
211
|
-
locationManagerDidChangeAuthorization(m: CLLocationManager) {
|
|
212
|
-
const st = m.authorizationStatus;
|
|
213
|
-
if (st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedWhenInUse ||
|
|
214
|
-
st === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedAlways) {
|
|
215
|
-
m.startUpdatingLocation();
|
|
216
|
-
} else if (st === CLAuthorizationStatus.kCLAuthorizationStatusDenied) {
|
|
217
|
-
cleanup();
|
|
218
|
-
reject(Object.assign(new Error('location permission denied'), { code: 'DENIED' }));
|
|
219
|
-
}
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
{ protocols: [CLLocationManagerDelegate] }
|
|
223
|
-
);
|
|
224
|
-
|
|
225
307
|
Utils.dispatchToMainThread(() => {
|
|
226
308
|
manager = CLLocationManager.new();
|
|
227
|
-
delegate =
|
|
309
|
+
delegate = GeoDelegate.new();
|
|
310
|
+
delegate.onResult = resolve;
|
|
311
|
+
delegate.onError = reject;
|
|
312
|
+
delegate.onCleanup = cleanup;
|
|
228
313
|
manager.delegate = delegate;
|
|
229
314
|
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
|
|
230
315
|
const st = manager.authorizationStatus;
|
|
@@ -238,31 +323,18 @@ export function registerExtendedHandlers(): void {
|
|
|
238
323
|
});
|
|
239
324
|
|
|
240
325
|
// ── photos (PHPickerViewController) ────────────────────────────────
|
|
241
|
-
bridge.register('photos.pick', ({ dataUrl, maxSize }:
|
|
326
|
+
bridge.register('photos.pick', ({ dataUrl, maxSize }: { dataUrl?: boolean; maxSize?: number } = {}) => {
|
|
242
327
|
if (!isIOS) throw Object.assign(new Error('iOS only for now'), { code: 'UNSUPPORTED' });
|
|
243
328
|
return new Promise((resolve) => {
|
|
244
329
|
Utils.dispatchToMainThread(() => {
|
|
245
330
|
const config = PHPickerConfiguration.new();
|
|
246
331
|
config.selectionLimit = 1;
|
|
247
|
-
const DelegateClass = (NSObject as any).extend(
|
|
248
|
-
{
|
|
249
|
-
pickerDidFinishPicking(picker: PHPickerViewController, results: NSArray<PHPickerResult>) {
|
|
250
|
-
picker.dismissViewControllerAnimatedCompletion(true, null);
|
|
251
|
-
const result = results.count > 0 ? results.objectAtIndex(0) : null;
|
|
252
|
-
if (!result) return resolve({ picked: false });
|
|
253
|
-
result.itemProvider.loadObjectOfClassCompletionHandler(UIImage.class(), (img: UIImage) => {
|
|
254
|
-
if (!img) return resolve({ picked: true });
|
|
255
|
-
const out: any = { picked: true, width: img.size.width, height: img.size.height };
|
|
256
|
-
if (dataUrl) out.dataUrl = uiImageToDataUrl(img, maxSize ?? 1024);
|
|
257
|
-
resolve(out);
|
|
258
|
-
});
|
|
259
|
-
},
|
|
260
|
-
},
|
|
261
|
-
{ protocols: [PHPickerViewControllerDelegate] }
|
|
262
|
-
);
|
|
263
332
|
const picker = PHPickerViewController.alloc().initWithConfiguration(config);
|
|
264
|
-
const delegate =
|
|
265
|
-
|
|
333
|
+
const delegate = PhotoPickerDelegate.new();
|
|
334
|
+
delegate.wantDataUrl = !!dataUrl;
|
|
335
|
+
delegate.maxSize = maxSize;
|
|
336
|
+
delegate.onResult = resolve;
|
|
337
|
+
(picker as any)._appwrapDelegate = delegate; // any: stash extra prop on VC to retain delegate (ARC)
|
|
266
338
|
picker.delegate = delegate;
|
|
267
339
|
Utils.ios.getRootViewController().presentViewControllerAnimatedCompletion(picker, true, null);
|
|
268
340
|
});
|
|
@@ -314,14 +386,16 @@ export function registerExtendedHandlers(): void {
|
|
|
314
386
|
function applyIosOrientation(mask: number): void {
|
|
315
387
|
setIosOrientationMask(mask);
|
|
316
388
|
Utils.dispatchToMainThread(() => {
|
|
317
|
-
const rootVC
|
|
389
|
+
const rootVC = Utils.ios.getRootViewController();
|
|
318
390
|
rootVC?.setNeedsUpdateOfSupportedInterfaceOrientations?.();
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
391
|
+
// any: deliberate runtime probe — reference the iOS-16 geometry class via `global` so PRE-iOS-16
|
|
392
|
+
// runtimes (where it's absent) hit the early-return instead of a hard symbol miss.
|
|
393
|
+
const Prefs: any = (global as any).UIWindowSceneGeometryPreferencesIOS;
|
|
394
|
+
const scenes = UIApplication.sharedApplication.connectedScenes?.allObjects;
|
|
322
395
|
if (!Prefs || !scenes) return; // pre-iOS-16: the mask alone takes effect on next rotation
|
|
323
396
|
for (let i = 0; i < scenes.count; i++) {
|
|
324
|
-
|
|
397
|
+
// any: ObjC errorHandler block is nullable — we pass `null`, which the typed (p1)=>void rejects.
|
|
398
|
+
const scene: any = scenes.objectAtIndex(i);
|
|
325
399
|
if (scene?.requestGeometryUpdateWithPreferencesErrorHandler) {
|
|
326
400
|
const prefs = Prefs.alloc().initWithInterfaceOrientations(mask);
|
|
327
401
|
scene.requestGeometryUpdateWithPreferencesErrorHandler(prefs, null);
|
|
@@ -330,7 +404,7 @@ function applyIosOrientation(mask: number): void {
|
|
|
330
404
|
});
|
|
331
405
|
}
|
|
332
406
|
|
|
333
|
-
let notifDelegate:
|
|
407
|
+
let notifDelegate: ForegroundNotificationDelegate | null = null; // retained for the app's lifetime
|
|
334
408
|
/**
|
|
335
409
|
* Install the UNUserNotificationCenter delegate. MUST run before
|
|
336
410
|
* `didFinishLaunching` returns, else a tap that COLD-LAUNCHES the app never
|
|
@@ -340,50 +414,7 @@ let notifDelegate: any = null; // retained for the app's lifetime
|
|
|
340
414
|
*/
|
|
341
415
|
export function installForegroundNotificationDelegate(): void {
|
|
342
416
|
if (!isIOS || notifDelegate) return;
|
|
343
|
-
|
|
344
|
-
{
|
|
345
|
-
userNotificationCenterWillPresentNotificationWithCompletionHandler(
|
|
346
|
-
_center: UNUserNotificationCenter,
|
|
347
|
-
notification: UNNotification,
|
|
348
|
-
completionHandler: (opts: number) => void
|
|
349
|
-
): void {
|
|
350
|
-
// Foreground REMOTE push (push trigger) → surface to kit.push.onMessage.
|
|
351
|
-
if (isRemotePush(notification)) onRemoteMessage(notification.request.content.userInfo, false);
|
|
352
|
-
// Present banner + list + sound + badge even while foreground (min target iOS 16).
|
|
353
|
-
completionHandler(
|
|
354
|
-
UNNotificationPresentationOptions.Banner |
|
|
355
|
-
UNNotificationPresentationOptions.List |
|
|
356
|
-
UNNotificationPresentationOptions.Sound |
|
|
357
|
-
UNNotificationPresentationOptions.Badge
|
|
358
|
-
);
|
|
359
|
-
},
|
|
360
|
-
// Notification tapped → route its deep link through the same path as an external open.
|
|
361
|
-
userNotificationCenterDidReceiveNotificationResponseWithCompletionHandler(
|
|
362
|
-
_center: UNUserNotificationCenter,
|
|
363
|
-
response: UNNotificationResponse,
|
|
364
|
-
completionHandler: () => void
|
|
365
|
-
): void {
|
|
366
|
-
// Tapped REMOTE push → kit.push.onTap (with payload). Local notifs keep the deep-link path below.
|
|
367
|
-
if (isRemotePush(response.notification)) onRemoteMessage(response.notification.request.content.userInfo, true);
|
|
368
|
-
// userInfo may come back as an NSDictionary or an auto-marshalled JS object.
|
|
369
|
-
const info: any = response.notification.request.content.userInfo;
|
|
370
|
-
const url = info ? (typeof info.objectForKey === 'function' ? info.objectForKey('url') : info.url) : null;
|
|
371
|
-
// Diagnostic breadcrumb (persists across the cold relaunch) — surfaced in the handshake's debug field.
|
|
372
|
-
try {
|
|
373
|
-
ApplicationSettings.setString(
|
|
374
|
-
'kit:__notifTap',
|
|
375
|
-
JSON.stringify({ at: Date.now(), url: url ? String(url) : null, hadInfo: !!info })
|
|
376
|
-
);
|
|
377
|
-
} catch {
|
|
378
|
-
/* diagnostic only */
|
|
379
|
-
}
|
|
380
|
-
if (url) onDeepLink(String(url));
|
|
381
|
-
completionHandler();
|
|
382
|
-
},
|
|
383
|
-
},
|
|
384
|
-
{ protocols: [UNUserNotificationCenterDelegate] }
|
|
385
|
-
);
|
|
386
|
-
notifDelegate = DelegateClass.new();
|
|
417
|
+
notifDelegate = ForegroundNotificationDelegate.new();
|
|
387
418
|
UNUserNotificationCenter.currentNotificationCenter().delegate = notifDelegate;
|
|
388
419
|
}
|
|
389
420
|
|