@livx.cc/appwrap 0.26.0 → 0.28.1
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/AndroidManifest.xml +3 -0
- package/runtime/app/app.ts +57 -39
- package/runtime/app/shell/android-helpers.ts +6 -6
- package/runtime/app/shell/background-bootstrap.generated.ts +8 -0
- package/runtime/app/shell/background-context.ts +20 -0
- package/runtime/app/shell/banner.ts +9 -4
- package/runtime/app/shell/bridge.ts +2 -0
- package/runtime/app/shell/capabilities.manifest.ts +22 -3
- 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 +25 -4
- package/runtime/app/shell/fcm-firebase.d.ts +2 -1
- package/runtime/app/shell/handlers-android.ts +16 -12
- package/runtime/app/shell/handlers-background.ts +290 -0
- 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 -15
- package/runtime/app/shell/handlers-reviews.ts +5 -5
- 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 +122 -1
- package/runtime/app/shell/handlers.ts +10 -3
- package/runtime/app/shell/ios-frameworks.d.ts +9 -44
- 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 +21 -0
- package/runtime/tsconfig.json +1 -0
- package/src/cli.ts +74 -18
- package/src/config.ts +20 -0
- package/src/derive.ts +75 -0
|
@@ -18,23 +18,55 @@ import { appwrapNativeLog } from './native-log';
|
|
|
18
18
|
const appendWebLog = appwrapNativeLog;
|
|
19
19
|
|
|
20
20
|
/** Parse a #rrggbb config color into a UIColor (for the WebView background). */
|
|
21
|
-
function uiColorFromHex(hex?: string):
|
|
21
|
+
function uiColorFromHex(hex?: string): UIColor | null {
|
|
22
22
|
const m = /^#?([0-9a-fA-F]{6})$/.exec((hex ?? '').trim());
|
|
23
23
|
if (!m) return null;
|
|
24
24
|
const n = parseInt(m[1], 16);
|
|
25
25
|
return UIColor.colorWithRedGreenBlueAlpha(((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255, 1);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** WKScriptMessageHandler for the `appwrap` channel — forwards JSON envelopes to the owning view. */
|
|
29
|
+
@NativeClass()
|
|
30
|
+
class AppwrapScriptHandler extends NSObject implements WKScriptMessageHandler {
|
|
31
|
+
static ObjCProtocols = [WKScriptMessageHandler];
|
|
32
|
+
owner!: WeakRef<CustomWebView>;
|
|
33
|
+
static new(): AppwrapScriptHandler {
|
|
34
|
+
return <AppwrapScriptHandler>super.new();
|
|
35
|
+
}
|
|
36
|
+
userContentControllerDidReceiveScriptMessage(_controller: WKUserContentController, message: WKScriptMessage): void {
|
|
37
|
+
const view = this.owner.get ? this.owner.get() : (this.owner as { deref(): CustomWebView }).deref();
|
|
38
|
+
const body = message.body;
|
|
39
|
+
// Envelopes always travel as JSON strings (protocol v1)
|
|
40
|
+
if (view?.onAppwrapMessage && typeof body === 'string') {
|
|
41
|
+
view.onAppwrapMessage(body);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** WKScriptMessageHandler for the `appwrapLog` channel — debug console-forwarding to the file sink. */
|
|
47
|
+
@NativeClass()
|
|
48
|
+
class AppwrapLogHandler extends NSObject implements WKScriptMessageHandler {
|
|
49
|
+
static ObjCProtocols = [WKScriptMessageHandler];
|
|
50
|
+
static new(): AppwrapLogHandler {
|
|
51
|
+
return <AppwrapLogHandler>super.new();
|
|
52
|
+
}
|
|
53
|
+
userContentControllerDidReceiveScriptMessage(_c: WKUserContentController, message: WKScriptMessage): void {
|
|
54
|
+
// Write to the file sink — NS console.log / NSLog don't surface to devicectl/idevicesyslog
|
|
55
|
+
// on a device build, but a file in the app container is reliably pullable.
|
|
56
|
+
if (typeof message.body === 'string') appendWebLog(message.body);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
28
60
|
export class CustomWebView extends WebView {
|
|
29
61
|
/** Set by the bridge before load; receives raw envelope JSON. */
|
|
30
62
|
onAppwrapMessage: ((json: string) => void) | null = null;
|
|
31
|
-
private _scriptHandler
|
|
32
|
-
private _logHandler
|
|
33
|
-
private _schemeHandler
|
|
63
|
+
private _scriptHandler!: AppwrapScriptHandler; // retained — WKUserContentController holds it weakly
|
|
64
|
+
private _logHandler!: AppwrapLogHandler; // retained — debug console-forwarding handler
|
|
65
|
+
private _schemeHandler!: WKURLSchemeHandler;
|
|
34
66
|
// NOT `_uiDelegate`: the base NativeScript WebView uses that exact field name, and
|
|
35
67
|
// TS `private` is compile-time only — sharing it let super.initNativeView() overwrite
|
|
36
68
|
// our media-capture delegate with NS's, so getUserMedia re-prompted every call.
|
|
37
|
-
private _appwrapUiDelegate
|
|
69
|
+
private _appwrapUiDelegate!: WKUIDelegate; // retained — WKWebView holds uiDelegate weakly
|
|
38
70
|
|
|
39
71
|
/** No-op on iOS: WKWebView suspends rAF/timers itself when the app leaves the foreground.
|
|
40
72
|
* Kept for parity with the Android impl (wired to suspend/resume in main-page). */
|
|
@@ -67,39 +99,14 @@ export class CustomWebView extends WebView {
|
|
|
67
99
|
}
|
|
68
100
|
|
|
69
101
|
const owner = new WeakRef<CustomWebView>(this);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
userContentControllerDidReceiveScriptMessage(
|
|
73
|
-
_controller: WKUserContentController,
|
|
74
|
-
message: WKScriptMessage
|
|
75
|
-
): void {
|
|
76
|
-
const view = owner.get ? owner.get() : (owner as any).deref();
|
|
77
|
-
const body = message.body;
|
|
78
|
-
// Envelopes always travel as JSON strings (protocol v1)
|
|
79
|
-
if (view?.onAppwrapMessage && typeof body === 'string') {
|
|
80
|
-
view.onAppwrapMessage(body);
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
{ protocols: [WKScriptMessageHandler] }
|
|
85
|
-
);
|
|
86
|
-
this._scriptHandler = HandlerClass.new();
|
|
102
|
+
this._scriptHandler = AppwrapScriptHandler.new();
|
|
103
|
+
this._scriptHandler.owner = owner;
|
|
87
104
|
config.userContentController.addScriptMessageHandlerName(this._scriptHandler, 'appwrap');
|
|
88
105
|
|
|
89
106
|
// Debug console-forwarding: web console/errors → NSLog (via NS console.log) → device console,
|
|
90
107
|
// so `appwrap logs ios` can stream them headlessly. Only wired in debug mode.
|
|
91
108
|
if (SHELL_CONFIG.debug) {
|
|
92
|
-
|
|
93
|
-
{
|
|
94
|
-
userContentControllerDidReceiveScriptMessage(_c: WKUserContentController, message: WKScriptMessage): void {
|
|
95
|
-
// Write to the file sink — NS console.log / NSLog don't surface to devicectl/idevicesyslog
|
|
96
|
-
// on a device build, but a file in the app container is reliably pullable.
|
|
97
|
-
if (typeof message.body === 'string') appendWebLog(message.body);
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
{ protocols: [WKScriptMessageHandler] }
|
|
101
|
-
);
|
|
102
|
-
this._logHandler = LogClass.new();
|
|
109
|
+
this._logHandler = AppwrapLogHandler.new();
|
|
103
110
|
config.userContentController.addScriptMessageHandlerName(this._logHandler, 'appwrapLog');
|
|
104
111
|
appendWebLog('[native] log handler registered'); // boot marker: proves the file sink works
|
|
105
112
|
}
|
|
@@ -124,13 +131,13 @@ export class CustomWebView extends WebView {
|
|
|
124
131
|
// Edge-swipe to go back/forward in WebView history — the iOS native gesture
|
|
125
132
|
// users expect. SPAs that own their own history still get it for free.
|
|
126
133
|
webView.allowsBackForwardNavigationGestures = true;
|
|
127
|
-
if (
|
|
128
|
-
|
|
134
|
+
if (webView.configuration.dataDetectorTypes !== undefined) {
|
|
135
|
+
webView.configuration.dataDetectorTypes = 0; // WKDataDetectorTypeNone (WKDataDetectorTypes.None)
|
|
129
136
|
}
|
|
130
137
|
|
|
131
138
|
// Observability (debug mode only): make the WebView debuggable via Safari Web Inspector
|
|
132
139
|
// (Develop → <device> → Para) — full console + network. iOS 16.4+ (guarded). Off in store builds.
|
|
133
|
-
if (SHELL_CONFIG.debug && webView.respondsToSelector('setInspectable:'))
|
|
140
|
+
if (SHELL_CONFIG.debug && webView.respondsToSelector('setInspectable:')) webView.inspectable = true;
|
|
134
141
|
|
|
135
142
|
this._appwrapUiDelegate = createUiDelegate();
|
|
136
143
|
webView.UIDelegate = this._appwrapUiDelegate;
|
|
@@ -147,9 +154,9 @@ export class CustomWebView extends WebView {
|
|
|
147
154
|
const wk = this.nativeViewProtected as WKWebView;
|
|
148
155
|
if (wk && this._appwrapUiDelegate) wk.UIDelegate = this._appwrapUiDelegate;
|
|
149
156
|
// Lightweight self-check surfaced via debug.webviewInfo (support diagnostics).
|
|
150
|
-
(global as
|
|
157
|
+
(global as { __appwrapWebviewDiag?: string }).__appwrapWebviewDiag =
|
|
151
158
|
`delegateIsMine=${!!wk && wk.UIDelegate === this._appwrapUiDelegate} ` +
|
|
152
|
-
`respondsToMedia=${!!wk && !!wk.UIDelegate && !!
|
|
159
|
+
`respondsToMedia=${!!wk && !!wk.UIDelegate && !!wk.UIDelegate.respondsToSelector?.('webView:requestMediaCapturePermissionForOrigin:initiatedByFrame:type:decisionHandler:')}`;
|
|
153
160
|
}
|
|
154
161
|
}
|
|
155
162
|
|
|
@@ -163,41 +170,42 @@ export class CustomWebView extends WebView {
|
|
|
163
170
|
* request body, so POSTs proxy fine.
|
|
164
171
|
* - else → 404 (or SPA fallback when there's no backend).
|
|
165
172
|
*/
|
|
166
|
-
function createAppSchemeHandler():
|
|
173
|
+
function createAppSchemeHandler(): WKURLSchemeHandler {
|
|
167
174
|
const wwwPath = nsPath.join(knownFolders.currentApp().path, 'www');
|
|
168
175
|
|
|
169
|
-
const serveFile = (task:
|
|
176
|
+
const serveFile = (task: WKURLSchemeTask, url: NSURL, filePath: string, ext: string): void => {
|
|
170
177
|
const data = NSData.dataWithContentsOfFile(filePath);
|
|
171
178
|
const mime = mimeFor(ext, filePath);
|
|
172
179
|
const response = NSHTTPURLResponse.alloc().initWithURLStatusCodeHTTPVersionHeaderFields(
|
|
173
180
|
url, 200, 'HTTP/1.1',
|
|
181
|
+
// NS auto-marshals this JS object into NSDictionary<string,string>
|
|
174
182
|
{
|
|
175
183
|
'Content-Type': `${mime}; charset=utf-8`,
|
|
176
184
|
'Content-Length': String(data?.length ?? 0),
|
|
177
185
|
'Access-Control-Allow-Origin': '*',
|
|
178
186
|
'Cache-Control': 'no-cache',
|
|
179
|
-
} as
|
|
187
|
+
} as unknown as NSDictionary<string, string>
|
|
180
188
|
);
|
|
181
189
|
task.didReceiveResponse(response);
|
|
182
190
|
if (data) task.didReceiveData(data);
|
|
183
191
|
task.didFinish();
|
|
184
192
|
};
|
|
185
193
|
|
|
186
|
-
const proxyToBackend = (task:
|
|
194
|
+
const proxyToBackend = (task: WKURLSchemeTask, url: NSURL, rel: string): void => {
|
|
187
195
|
const backend = SHELL_CONFIG.backendOrigin.replace(/\/+$/, '');
|
|
188
196
|
const query = url.query ? `?${url.query}` : '';
|
|
189
197
|
const req = NSMutableURLRequest.requestWithURL(NSURL.URLWithString(`${backend}/${rel}${query}`));
|
|
190
198
|
req.HTTPMethod = String(task.request.HTTPMethod ?? 'GET');
|
|
191
199
|
const hdrs = task.request.allHTTPHeaderFields;
|
|
192
200
|
if (hdrs) req.allHTTPHeaderFields = hdrs;
|
|
193
|
-
req.setValueForHTTPHeaderField(null as
|
|
201
|
+
req.setValueForHTTPHeaderField(null as unknown as string, 'Host'); // let URLSession set Host from the target URL
|
|
194
202
|
// WKURLSchemeHandler delivers a POST/PUT body via HTTPBodyStream (not HTTPBody) — read it.
|
|
195
|
-
let body = task.request.HTTPBody;
|
|
203
|
+
let body: NSData = task.request.HTTPBody;
|
|
196
204
|
if (!body && task.request.HTTPBodyStream) {
|
|
197
205
|
const stream = task.request.HTTPBodyStream;
|
|
198
206
|
const acc = NSMutableData.alloc().init();
|
|
199
207
|
const cap = 16384;
|
|
200
|
-
const buf =
|
|
208
|
+
const buf = interop.alloc(cap);
|
|
201
209
|
stream.open();
|
|
202
210
|
while (stream.hasBytesAvailable) {
|
|
203
211
|
const n = stream.readMaxLength(buf, cap);
|
|
@@ -211,11 +219,11 @@ function createAppSchemeHandler(): any {
|
|
|
211
219
|
|
|
212
220
|
const dataTask = NSURLSession.sharedSession.dataTaskWithRequestCompletionHandler(
|
|
213
221
|
req,
|
|
214
|
-
(data:
|
|
222
|
+
(data: NSData, resp: NSURLResponse, error: NSError) => {
|
|
215
223
|
try {
|
|
216
224
|
if (error || !resp) {
|
|
217
225
|
const r = NSHTTPURLResponse.alloc().initWithURLStatusCodeHTTPVersionHeaderFields(
|
|
218
|
-
url, 502, 'HTTP/1.1', { 'Content-Length': '0' } as
|
|
226
|
+
url, 502, 'HTTP/1.1', { 'Content-Length': '0' } as unknown as NSDictionary<string, string>
|
|
219
227
|
);
|
|
220
228
|
task.didReceiveResponse(r);
|
|
221
229
|
task.didFinish();
|
|
@@ -223,10 +231,12 @@ function createAppSchemeHandler(): any {
|
|
|
223
231
|
}
|
|
224
232
|
// Re-emit under the app:// URL (same-origin). Strip Content-Encoding (URLSession already
|
|
225
233
|
// decoded the body) and recompute length; keep the upstream Content-Type + status.
|
|
226
|
-
|
|
234
|
+
// resp is NSHTTPURLResponse at runtime (HTTP request) — cast for the HTTP-only members.
|
|
235
|
+
const httpResp = resp as NSHTTPURLResponse;
|
|
236
|
+
const ct = httpResp.valueForHTTPHeaderField?.('Content-Type') ?? 'application/octet-stream';
|
|
227
237
|
const out = NSHTTPURLResponse.alloc().initWithURLStatusCodeHTTPVersionHeaderFields(
|
|
228
|
-
url,
|
|
229
|
-
{ 'Content-Type': ct, 'Content-Length': String(data?.length ?? 0), 'Cache-Control': 'no-cache' } as
|
|
238
|
+
url, httpResp.statusCode ?? 200, 'HTTP/1.1',
|
|
239
|
+
{ 'Content-Type': ct, 'Content-Length': String(data?.length ?? 0), 'Cache-Control': 'no-cache' } as unknown as NSDictionary<string, string>
|
|
230
240
|
);
|
|
231
241
|
task.didReceiveResponse(out);
|
|
232
242
|
if (data) task.didReceiveData(data);
|
|
@@ -239,36 +249,38 @@ function createAppSchemeHandler(): any {
|
|
|
239
249
|
dataTask.resume();
|
|
240
250
|
};
|
|
241
251
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
252
|
+
@NativeClass()
|
|
253
|
+
class AppSchemeHandler extends NSObject implements WKURLSchemeHandler {
|
|
254
|
+
static ObjCProtocols = [WKURLSchemeHandler];
|
|
255
|
+
static new(): AppSchemeHandler {
|
|
256
|
+
return <AppSchemeHandler>super.new();
|
|
257
|
+
}
|
|
258
|
+
webViewStartURLSchemeTask(_webView: WKWebView, task: WKURLSchemeTask): void {
|
|
259
|
+
const url = task.request.URL;
|
|
260
|
+
let rel = decodeURIComponent(String(url.path ?? '/')).replace(/^\/+/, '');
|
|
261
|
+
if (!rel) rel = SHELL_CONFIG.entry;
|
|
262
|
+
const filePath = nsPath.join(wwwPath, rel);
|
|
263
|
+
const ext = rel.includes('.') ? rel.split('.').pop()!.toLowerCase() : '';
|
|
250
264
|
|
|
251
|
-
|
|
265
|
+
if (File.exists(filePath)) { serveFile(task, url, filePath, ext); return; }
|
|
252
266
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
267
|
+
const method = String(task.request.HTTPMethod ?? 'GET').toUpperCase();
|
|
268
|
+
const accept = task.request.valueForHTTPHeaderField?.('Accept') ?? '';
|
|
269
|
+
const isNavigation = method === 'GET' && accept.indexOf('text/html') >= 0;
|
|
256
270
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
271
|
+
if (isNavigation && !ext) { serveFile(task, url, nsPath.join(wwwPath, SHELL_CONFIG.entry), 'html'); return; }
|
|
272
|
+
if (SHELL_CONFIG.backendOrigin) { proxyToBackend(task, url, rel); return; }
|
|
273
|
+
if (!ext) { serveFile(task, url, nsPath.join(wwwPath, SHELL_CONFIG.entry), 'html'); return; }
|
|
260
274
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
);
|
|
273
|
-
return HandlerClass.new();
|
|
275
|
+
const resp = NSHTTPURLResponse.alloc().initWithURLStatusCodeHTTPVersionHeaderFields(
|
|
276
|
+
url, 404, 'HTTP/1.1', { 'Content-Length': '0' } as unknown as NSDictionary<string, string>
|
|
277
|
+
);
|
|
278
|
+
task.didReceiveResponse(resp);
|
|
279
|
+
task.didFinish();
|
|
280
|
+
}
|
|
281
|
+
webViewStopURLSchemeTask(_webView: WKWebView, _task: WKURLSchemeTask): void {
|
|
282
|
+
// single-shot responses; in-flight proxy completions are guarded by try/catch.
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return AppSchemeHandler.new();
|
|
274
286
|
}
|
|
@@ -19,7 +19,7 @@ export function startDevMenu(): void {
|
|
|
19
19
|
|
|
20
20
|
// ── shake detection (total acceleration magnitude in g incl. gravity, debounced) ──
|
|
21
21
|
const SHAKE_G = 1.8; // spike vs. ~1g rest
|
|
22
|
-
let shakeMm:
|
|
22
|
+
let shakeMm: CMMotionManager | null = null; // hold a ref so CoreMotion isn't GC'd
|
|
23
23
|
let firstSpikeAt = 0;
|
|
24
24
|
let spikes = 0;
|
|
25
25
|
let lastMenuAt = 0;
|
|
@@ -68,7 +68,7 @@ function startAndroidShake(): void {
|
|
|
68
68
|
const G = 9.80665;
|
|
69
69
|
const listener = new android.hardware.SensorEventListener({
|
|
70
70
|
onAccuracyChanged() {},
|
|
71
|
-
onSensorChanged(e:
|
|
71
|
+
onSensorChanged(e: android.hardware.SensorEvent) {
|
|
72
72
|
const v = e.values;
|
|
73
73
|
onAccelMagnitude(Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) / G);
|
|
74
74
|
},
|
package/runtime/app/shell/env.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { Application, isAndroid } from '@nativescript/core';
|
|
2
2
|
|
|
3
|
-
declare const android: any, NSProcessInfo: any, UIAccessibilityIsReduceMotionEnabled: any;
|
|
4
|
-
|
|
5
3
|
/**
|
|
6
4
|
* Runtime environment hints the page can't reliably detect itself, surfaced natively so a PWA can
|
|
7
5
|
* scale its rendering down inside the shell. Injected at document-start as `window.__APPWRAP__` (see
|
|
@@ -36,7 +34,7 @@ export function envGlobalsJs(): string {
|
|
|
36
34
|
function androidIsEmulator(): boolean {
|
|
37
35
|
try {
|
|
38
36
|
const B = android.os.Build;
|
|
39
|
-
const has = (v:
|
|
37
|
+
const has = (v: string, s: string) => String(v || '').toLowerCase().includes(s);
|
|
40
38
|
return (
|
|
41
39
|
has(B.FINGERPRINT, 'generic') || has(B.FINGERPRINT, 'emulator') || has(B.FINGERPRINT, 'unknown') ||
|
|
42
40
|
has(B.MODEL, 'google_sdk') || has(B.MODEL, 'emulator') || has(B.MODEL, 'android sdk built for') ||
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Application, Connectivity, isAndroid } from '@nativescript/core';
|
|
2
|
+
import type { AndroidActivityNewIntentEventData, OrientationChangedEventData } from '@nativescript/core';
|
|
2
3
|
import { bridge } from './bridge';
|
|
3
4
|
import { connectivityStatus } from './handlers-extended';
|
|
4
5
|
|
|
5
6
|
let pendingDeepLink: string | null = null;
|
|
6
7
|
let pendingPushTap: { data: Record<string, string> } | null = null;
|
|
8
|
+
let pendingShortcut: string | null = null;
|
|
7
9
|
// True only once the PWA's JS has handshaked — i.e. the WebView is actually
|
|
8
10
|
// running our bundle and about to subscribe. Native page-load is too early:
|
|
9
11
|
// a deeplink.open emitted then lands in a WebView with no listener and is lost
|
|
@@ -27,6 +29,15 @@ export function onDeepLink(url: string): void {
|
|
|
27
29
|
else pendingDeepLink = url; // buffer until the PWA handshakes
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
/** A home-screen shortcut was activated (iOS performActionForShortcutItem / cold-start launchOptions;
|
|
33
|
+
* Android the launch intent's `appwrap_shortcut` extra). Buffered until the handshake like deep links
|
|
34
|
+
* (cold-start: a shortcut that launched the app would otherwise land in a WebView with no listener). */
|
|
35
|
+
export function onShortcut(id: string): void {
|
|
36
|
+
if (!id) return;
|
|
37
|
+
if (pwaReady) bridge.emit('app.shortcut', { id });
|
|
38
|
+
else pendingShortcut = id;
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
/** Android: a tray notification (FCM) was tapped → re-launched the activity with the data payload as
|
|
31
42
|
* intent extras. Buffered until handshake like deep links (cold-start-from-notification). iOS routes
|
|
32
43
|
* taps via the AppDelegate (handlers-push onRemoteMessage). */
|
|
@@ -53,13 +64,18 @@ export function onPwaHandshake(): void {
|
|
|
53
64
|
pendingPushTap = null;
|
|
54
65
|
setTimeout(() => bridge.emit('push.tap', payload), 500);
|
|
55
66
|
}
|
|
67
|
+
if (pendingShortcut) {
|
|
68
|
+
const id = pendingShortcut;
|
|
69
|
+
pendingShortcut = null;
|
|
70
|
+
setTimeout(() => bridge.emit('app.shortcut', { id }), 500);
|
|
71
|
+
}
|
|
56
72
|
}
|
|
57
73
|
|
|
58
74
|
/** Wire lifecycle + connectivity event forwarding (the PWA subscribes to these). */
|
|
59
75
|
export function startEventForwarding(): void {
|
|
60
76
|
Application.on(Application.suspendEvent, () => bridge.emit('app.pause'));
|
|
61
77
|
Application.on(Application.resumeEvent, () => bridge.emit('app.resume'));
|
|
62
|
-
Application.on(Application.orientationChangedEvent, (args:
|
|
78
|
+
Application.on(Application.orientationChangedEvent, (args: OrientationChangedEventData) =>
|
|
63
79
|
bridge.emit('screen.orientation.change', args?.newValue === 'landscape' ? 'landscape' : 'portrait')
|
|
64
80
|
);
|
|
65
81
|
|
|
@@ -71,18 +87,23 @@ export function startEventForwarding(): void {
|
|
|
71
87
|
/** Intents: launch intent (cold start) + onNewIntent (warm, singleTask). Carries both VIEW deep links
|
|
72
88
|
* and FCM notification-tap extras. */
|
|
73
89
|
function wireAndroidDeepLinks(): void {
|
|
74
|
-
const emitFromIntent = (intent:
|
|
90
|
+
const emitFromIntent = (intent: android.content.Intent | null | undefined) => {
|
|
75
91
|
try {
|
|
76
92
|
const data = intent?.getData?.();
|
|
77
93
|
if (data) onDeepLink(String(data.toString()));
|
|
78
94
|
const tap = readFcmTapExtras(intent);
|
|
79
95
|
if (tap) onPushTap(tap);
|
|
96
|
+
const shortcutId = intent?.getStringExtra?.('appwrap_shortcut');
|
|
97
|
+
if (shortcutId) {
|
|
98
|
+
onShortcut(String(shortcutId));
|
|
99
|
+
intent.removeExtra?.('appwrap_shortcut'); // consume — don't re-fire on the next relayout read
|
|
100
|
+
}
|
|
80
101
|
} catch (e) {
|
|
81
102
|
console.warn('AppWrap: intent read failed', e);
|
|
82
103
|
}
|
|
83
104
|
};
|
|
84
105
|
|
|
85
|
-
Application.android.on(Application.android.activityNewIntentEvent
|
|
106
|
+
Application.android.on(Application.android.activityNewIntentEvent, (args: AndroidActivityNewIntentEventData) =>
|
|
86
107
|
emitFromIntent(args.intent)
|
|
87
108
|
);
|
|
88
109
|
emitFromIntent(Application.android.startActivity?.getIntent?.());
|
|
@@ -91,7 +112,7 @@ function wireAndroidDeepLinks(): void {
|
|
|
91
112
|
/** A tapped FCM notification re-launches the activity with the message's data payload as string
|
|
92
113
|
* extras, alongside FCM's own `google.*`/`gcm.*`/`from`/`collapse_key` bookkeeping keys. Detect via
|
|
93
114
|
* those markers; return the app's data keys only. Null when this isn't an FCM-originated intent. */
|
|
94
|
-
function readFcmTapExtras(intent:
|
|
115
|
+
function readFcmTapExtras(intent: android.content.Intent | null | undefined): { data: Record<string, string> } | null {
|
|
95
116
|
try {
|
|
96
117
|
const extras = intent?.getExtras?.();
|
|
97
118
|
if (!extras || (!extras.containsKey('google.message_id') && !extras.containsKey('from'))) return null;
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Minimal ambient declarations for the Firebase Messaging Android classes the FCM service extends.
|
|
3
3
|
* The full classes are provided at runtime by the `firebase-messaging` AAR (added by the CLI only
|
|
4
4
|
* when push is wired) — the NativeScript Android runtime marshals them. Declare-merges into the
|
|
5
|
-
* `com` namespace from @nativescript/types-android.
|
|
5
|
+
* `com` namespace from @nativescript/types-android. (iOS SDK frameworks are typed via the
|
|
6
|
+
* @nativescript/types-ios references in references.d.ts.)
|
|
6
7
|
*/
|
|
7
8
|
declare namespace com.google.firebase.messaging {
|
|
8
9
|
class RemoteMessage {
|
|
@@ -2,7 +2,8 @@ import { Application, Utils } from '@nativescript/core';
|
|
|
2
2
|
import { bridge } from './bridge';
|
|
3
3
|
import { requestPermissions, startActivityForResult, uriToDataUrl, bitmapToDataUrl } from './android-helpers';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// no NS types: android-32 typings omit ContactsContract/MediaStore column + ACTION_PICK_IMAGES constants this file reads
|
|
6
|
+
declare const android: any, androidx: any;
|
|
6
7
|
|
|
7
8
|
const err = (code: string, message: string) => Object.assign(new Error(message), { code });
|
|
8
9
|
|
|
@@ -125,18 +126,21 @@ export function registerAndroidHandlers(): void {
|
|
|
125
126
|
// ── geolocation (LocationManager) ──────────────────────────────────
|
|
126
127
|
const GEO_PERMS = ['android.permission.ACCESS_FINE_LOCATION', 'android.permission.ACCESS_COARSE_LOCATION'];
|
|
127
128
|
const locationManager = () => context().getSystemService(android.content.Context.LOCATION_SERVICE);
|
|
128
|
-
const toPosition = (loc:
|
|
129
|
+
const toPosition = (loc: android.location.Location) => ({
|
|
129
130
|
lat: loc.getLatitude(), lng: loc.getLongitude(), accuracy: loc.getAccuracy(),
|
|
130
131
|
});
|
|
131
|
-
const makeListener = (onLocation: (loc:
|
|
132
|
+
const makeListener = (onLocation: (loc: android.location.Location) => void) =>
|
|
132
133
|
new android.location.LocationListener({
|
|
133
|
-
// Both overloads dispatch here — newer Android may deliver a List<Location
|
|
134
|
-
onLocationChanged(
|
|
134
|
+
// Both overloads dispatch here — newer Android may deliver a List<Location>.
|
|
135
|
+
onLocationChanged(arg: android.location.Location | java.util.List<android.location.Location>) {
|
|
136
|
+
// interop: runtime duck-typing across the two Java overloads; TS can't narrow Java types here
|
|
137
|
+
let loc: any = arg;
|
|
135
138
|
if (loc && typeof loc.getLatitude !== 'function' && typeof loc.size === 'function') {
|
|
136
139
|
loc = loc.size() > 0 ? loc.get(loc.size() - 1) : null;
|
|
137
140
|
}
|
|
138
141
|
if (loc) onLocation(loc);
|
|
139
142
|
},
|
|
143
|
+
onFlushComplete() {},
|
|
140
144
|
onStatusChanged() {}, onProviderEnabled() {}, onProviderDisabled() {},
|
|
141
145
|
});
|
|
142
146
|
|
|
@@ -148,7 +152,7 @@ export function registerAndroidHandlers(): void {
|
|
|
148
152
|
?? lm.getLastKnownLocation(android.location.LocationManager.NETWORK_PROVIDER);
|
|
149
153
|
if (last) return resolve(toPosition(last));
|
|
150
154
|
|
|
151
|
-
let listener:
|
|
155
|
+
let listener: android.location.LocationListener | null = null;
|
|
152
156
|
const timer = setTimeout(() => {
|
|
153
157
|
if (listener) lm.removeUpdates(listener);
|
|
154
158
|
reject(err('TIMEOUT', 'location timeout'));
|
|
@@ -165,7 +169,7 @@ export function registerAndroidHandlers(): void {
|
|
|
165
169
|
});
|
|
166
170
|
});
|
|
167
171
|
|
|
168
|
-
let geoWatchListener:
|
|
172
|
+
let geoWatchListener: android.location.LocationListener | null = null;
|
|
169
173
|
bridge.register('geo.watch.start', async () => {
|
|
170
174
|
if (geoWatchListener) return; // already streaming
|
|
171
175
|
if (!(await requestPermissions(GEO_PERMS))) throw err('DENIED', 'location permission denied');
|
|
@@ -249,7 +253,7 @@ export function registerAndroidHandlers(): void {
|
|
|
249
253
|
|
|
250
254
|
// ── motion (SensorManager: accelerometer + gyroscope) ──────────────
|
|
251
255
|
const sensorManager = () => context().getSystemService(android.content.Context.SENSOR_SERVICE);
|
|
252
|
-
let motionListener:
|
|
256
|
+
let motionListener: android.hardware.SensorEventListener | null = null;
|
|
253
257
|
const lastMotion = { ax: 0, ay: 0, az: 0, rx: 0, ry: 0, rz: 0 };
|
|
254
258
|
|
|
255
259
|
bridge.register('motion.start', () => {
|
|
@@ -261,7 +265,7 @@ export function registerAndroidHandlers(): void {
|
|
|
261
265
|
let last = 0;
|
|
262
266
|
motionListener = new android.hardware.SensorEventListener({
|
|
263
267
|
onAccuracyChanged() {},
|
|
264
|
-
onSensorChanged(event:
|
|
268
|
+
onSensorChanged(event: android.hardware.SensorEvent) {
|
|
265
269
|
const v = event.values;
|
|
266
270
|
if (event.sensor.getType() === android.hardware.Sensor.TYPE_ACCELEROMETER) {
|
|
267
271
|
// Android accelerometer already reports m/s² incl. gravity — kit contract matches.
|
|
@@ -296,7 +300,7 @@ export function registerAndroidHandlers(): void {
|
|
|
296
300
|
if (resultCode !== android.app.Activity.RESULT_OK || !result?.getData()) return { picked: false };
|
|
297
301
|
|
|
298
302
|
const cr = context().getContentResolver();
|
|
299
|
-
const column = (uri:
|
|
303
|
+
const column = (uri: android.net.Uri, col: string, sel: string | null, args: string[] | null): string[] => {
|
|
300
304
|
const out: string[] = [];
|
|
301
305
|
const cursor = cr.query(uri, [col], sel, args, null);
|
|
302
306
|
if (cursor) {
|
|
@@ -327,7 +331,7 @@ export function registerAndroidHandlers(): void {
|
|
|
327
331
|
}
|
|
328
332
|
const CC = android.provider.ContactsContract;
|
|
329
333
|
const cr = context().getContentResolver();
|
|
330
|
-
const column = (uri:
|
|
334
|
+
const column = (uri: android.net.Uri, col: string, sel: string | null, args: string[] | null): string[] => {
|
|
331
335
|
const out: string[] = [];
|
|
332
336
|
const cursor = cr.query(uri, [col], sel, args, null);
|
|
333
337
|
if (cursor) {
|
|
@@ -415,7 +419,7 @@ export function registerAndroidHandlers(): void {
|
|
|
415
419
|
dir.mkdirs();
|
|
416
420
|
const uris = new java.util.ArrayList();
|
|
417
421
|
const mimes = new java.util.HashSet();
|
|
418
|
-
(files ?? []).forEach((f:
|
|
422
|
+
(files ?? []).forEach((f: { name: string; mimeType: string; base64: string }, i: number) => {
|
|
419
423
|
const bytes = android.util.Base64.decode(f.base64, android.util.Base64.DEFAULT);
|
|
420
424
|
const out = new java.io.File(dir, i + '-' + (f.name || 'file')); // index-prefix: avoid same-name collisions
|
|
421
425
|
const fos = new java.io.FileOutputStream(out);
|