@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 +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 +83 -11
- package/src/config.ts +21 -0
|
@@ -15,21 +15,31 @@ const err = (code: string, message: string) => Object.assign(new Error(message),
|
|
|
15
15
|
// empty protocols list it registers under the wrong selector and AVFoundation never calls it — the
|
|
16
16
|
// scanner opens but never decodes. The first decoded object is forwarded via the `onResult` instance
|
|
17
17
|
// field (assigned after `new()`), replacing the original closure capture.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
18
|
+
// Built lazily INSIDE the iOS-only path (mirrors banner.ts): NSObject/AV* are iOS globals and this file
|
|
19
|
+
// is imported on Android too, so a top-level `@NativeClass extends NSObject` would crash at module load.
|
|
20
|
+
// any: runtime-built ObjC subclass holder; the class BODY stays fully typed.
|
|
21
|
+
let MetadataDelegate: any;
|
|
22
|
+
function metadataDelegateClass(): any {
|
|
23
|
+
if (!MetadataDelegate) {
|
|
24
|
+
@NativeClass()
|
|
25
|
+
class MetadataDelegateImpl extends NSObject implements AVCaptureMetadataOutputObjectsDelegate {
|
|
26
|
+
static ObjCProtocols = [AVCaptureMetadataOutputObjectsDelegate];
|
|
27
|
+
onResult?: (obj: AVMetadataObject) => void;
|
|
28
|
+
static new(): MetadataDelegateImpl {
|
|
29
|
+
return <MetadataDelegateImpl>super.new();
|
|
30
|
+
}
|
|
31
|
+
captureOutputDidOutputMetadataObjectsFromConnection(
|
|
32
|
+
_output: AVCaptureOutput,
|
|
33
|
+
metadataObjects: NSArray<AVMetadataObject> | AVMetadataObject[],
|
|
34
|
+
_connection: AVCaptureConnection
|
|
35
|
+
): void {
|
|
36
|
+
const obj = (metadataObjects as NSArray<AVMetadataObject>)?.firstObject;
|
|
37
|
+
if (obj) this.onResult?.(obj);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
MetadataDelegate = MetadataDelegateImpl;
|
|
32
41
|
}
|
|
42
|
+
return MetadataDelegate;
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
/**
|
|
@@ -124,8 +134,8 @@ function registerIos(): void {
|
|
|
124
134
|
|
|
125
135
|
// Delegate fires on a dedicated serial queue (see dispatch_queue_create below); settle()
|
|
126
136
|
// hops back to the main thread for the WKWebView bridge reply.
|
|
127
|
-
const delegate =
|
|
128
|
-
delegate.onResult = (obj) => {
|
|
137
|
+
const delegate = metadataDelegateClass().new();
|
|
138
|
+
delegate.onResult = (obj: AVMetadataObject) => {
|
|
129
139
|
if (!pending) return;
|
|
130
140
|
const value = (obj as AVMetadataMachineReadableCodeObject).stringValue;
|
|
131
141
|
if (value == null) return;
|
|
@@ -4,23 +4,6 @@ import { requestPermissions } from './android-helpers';
|
|
|
4
4
|
|
|
5
5
|
const err = (code: string, message: string) => Object.assign(new Error(message), { code });
|
|
6
6
|
|
|
7
|
-
// iOS speech-synthesizer delegate — both finish & cancel settle the pending speak (the JS
|
|
8
|
-
// `speakPending` is captured via the `onSettle` instance field, assigned after `new()`).
|
|
9
|
-
@NativeClass()
|
|
10
|
-
class SpeakDelegate extends NSObject implements AVSpeechSynthesizerDelegate {
|
|
11
|
-
static ObjCProtocols = [AVSpeechSynthesizerDelegate];
|
|
12
|
-
onSettle?: () => void;
|
|
13
|
-
static new(): SpeakDelegate {
|
|
14
|
-
return <SpeakDelegate>super.new();
|
|
15
|
-
}
|
|
16
|
-
speechSynthesizerDidFinishSpeechUtterance(_s: AVSpeechSynthesizer, _u: AVSpeechUtterance): void {
|
|
17
|
-
this.onSettle?.();
|
|
18
|
-
}
|
|
19
|
-
speechSynthesizerDidCancelSpeechUtterance(_s: AVSpeechSynthesizer, _u: AVSpeechUtterance): void {
|
|
20
|
-
this.onSettle?.();
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
7
|
/**
|
|
25
8
|
* Voice I/O. TTS: iOS AVSpeechSynthesizer / Android TextToSpeech — speak resolves on finish.
|
|
26
9
|
* STT: iOS SFSpeechRecognizer + AVAudioEngine mic tap / Android SpeechRecognizer — listen resolves
|
|
@@ -37,6 +20,26 @@ export function registerSpeechHandlers(): void {
|
|
|
37
20
|
}
|
|
38
21
|
|
|
39
22
|
function registerIos(): void {
|
|
23
|
+
// iOS speech-synthesizer delegate — both finish & cancel settle the pending speak (the JS
|
|
24
|
+
// `speakPending` is captured via the `onSettle` instance field, assigned after `new()`). Declared
|
|
25
|
+
// INSIDE this iOS-only registrar (mirrors app.ts): NSObject/AV* are iOS globals and this shared
|
|
26
|
+
// module is imported on Android too, so a top-level `@NativeClass extends NSObject` would crash at
|
|
27
|
+
// module load.
|
|
28
|
+
@NativeClass()
|
|
29
|
+
class SpeakDelegate extends NSObject implements AVSpeechSynthesizerDelegate {
|
|
30
|
+
static ObjCProtocols = [AVSpeechSynthesizerDelegate];
|
|
31
|
+
onSettle?: () => void;
|
|
32
|
+
static new(): SpeakDelegate {
|
|
33
|
+
return <SpeakDelegate>super.new();
|
|
34
|
+
}
|
|
35
|
+
speechSynthesizerDidFinishSpeechUtterance(_s: AVSpeechSynthesizer, _u: AVSpeechUtterance): void {
|
|
36
|
+
this.onSettle?.();
|
|
37
|
+
}
|
|
38
|
+
speechSynthesizerDidCancelSpeechUtterance(_s: AVSpeechSynthesizer, _u: AVSpeechUtterance): void {
|
|
39
|
+
this.onSettle?.();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
40
43
|
// Strong refs while a session is live — ARC would otherwise free the synthesizer/engine/delegate
|
|
41
44
|
// the moment the JS locals fall out of scope (same hazard as handlers-scanner/oauth).
|
|
42
45
|
let synth: AVSpeechSynthesizer | null = null;
|
|
@@ -9,14 +9,24 @@ const err = (code: string, message: string) => Object.assign(new Error(message),
|
|
|
9
9
|
// (orphaned tracking window / throttled renderer), so recover from its didFinish. Strong-ref the
|
|
10
10
|
// delegate while the browser is up (ARC would otherwise free it). Mirrors the OAuth delegate pattern.
|
|
11
11
|
let activeSafariDelegate: NSObject | null = null;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
// Built lazily INSIDE an iOS-only path (mirrors banner.ts): NSObject/SF* are iOS globals and this file
|
|
13
|
+
// is imported on Android too, so a top-level `@NativeClass extends NSObject` would crash at module load.
|
|
14
|
+
// any: runtime-built ObjC subclass holder; the class BODY stays fully typed.
|
|
15
|
+
let SafariDelegate: any;
|
|
16
|
+
function safariDelegateClass(): any {
|
|
17
|
+
if (!SafariDelegate) {
|
|
18
|
+
@NativeClass()
|
|
19
|
+
class SafariDelegateImpl extends NSObject implements SFSafariViewControllerDelegate {
|
|
20
|
+
static ObjCProtocols = [SFSafariViewControllerDelegate];
|
|
21
|
+
static new(): SafariDelegateImpl { return <SafariDelegateImpl>super.new(); }
|
|
22
|
+
safariViewControllerDidFinish(_controller: SFSafariViewController): void {
|
|
23
|
+
activeSafariDelegate = null;
|
|
24
|
+
bridge.getWebView()?.recoverAfterNativeSurface();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
SafariDelegate = SafariDelegateImpl;
|
|
19
28
|
}
|
|
29
|
+
return SafariDelegate;
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
/** Running on a simulator/emulator? (mirrors env.ts detection; kept local to avoid a cross-import). */
|
|
@@ -204,7 +214,7 @@ export function registerSystemHandlers(): void {
|
|
|
204
214
|
Utils.dispatchToMainThread(() => {
|
|
205
215
|
const vc = SFSafariViewController.alloc().initWithURL(NSURL.URLWithString(target));
|
|
206
216
|
if (toolbarColor) vc.preferredControlTintColor = new Color(String(toolbarColor)).ios;
|
|
207
|
-
activeSafariDelegate =
|
|
217
|
+
activeSafariDelegate = safariDelegateClass().new();
|
|
208
218
|
vc.delegate = activeSafariDelegate as SFSafariViewControllerDelegate;
|
|
209
219
|
Utils.ios.getRootViewController().presentViewControllerAnimatedCompletion(vc, true, null);
|
|
210
220
|
});
|
|
@@ -11,123 +11,134 @@ import { Utils } from '@nativescript/core';
|
|
|
11
11
|
* window.alert/confirm/prompt.
|
|
12
12
|
*
|
|
13
13
|
* Returns a retained delegate instance (the WKWebView holds uiDelegate weakly).
|
|
14
|
+
*
|
|
15
|
+
* Built lazily INSIDE createUiDelegate (mirrors banner.ts): NSObject/WK* are iOS globals, and although
|
|
16
|
+
* this module is reached only from the iOS WebView path today, defining the class at top level would
|
|
17
|
+
* still evaluate those globals at ES-module instantiation — keep it inside the factory to stay safe.
|
|
14
18
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// any: runtime-built ObjC subclass holder; the class BODY stays fully typed.
|
|
20
|
+
let AppwrapUiDelegate: any;
|
|
21
|
+
function appwrapUiDelegateClass(): any {
|
|
22
|
+
if (AppwrapUiDelegate) return AppwrapUiDelegate;
|
|
23
|
+
@NativeClass()
|
|
24
|
+
class AppwrapUiDelegateImpl extends NSObject implements WKUIDelegate {
|
|
25
|
+
static ObjCProtocols = [WKUIDelegate];
|
|
26
|
+
static new(): AppwrapUiDelegateImpl {
|
|
27
|
+
return <AppwrapUiDelegateImpl>super.new();
|
|
28
|
+
}
|
|
21
29
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
// iOS 15+: WKUIDelegate media capture permission.
|
|
31
|
+
webViewRequestMediaCapturePermissionForOriginInitiatedByFrameTypeDecisionHandler(
|
|
32
|
+
_webView: WKWebView,
|
|
33
|
+
_origin: WKSecurityOrigin,
|
|
34
|
+
_frame: WKFrameInfo,
|
|
35
|
+
_type: WKMediaCaptureType,
|
|
36
|
+
decisionHandler: (decision: WKPermissionDecision) => void
|
|
37
|
+
): void {
|
|
38
|
+
// WKPermissionDecision.Grant === 1 — silently grant (the app already holds the
|
|
39
|
+
// OS-level mic/camera permission; media.ensurePermission established TCC).
|
|
40
|
+
// NOTE: this only ever sees capabilities the build DECLARED — a request for an
|
|
41
|
+
// UNDECLARED capability is rejected upstream in JS (mediaCaptureGuardJs), because
|
|
42
|
+
// WebKit asks TCC to authorize the device AROUND this callback and a missing usage
|
|
43
|
+
// string is a hard process-kill a native `deny` here cannot prevent. So denying
|
|
44
|
+
// here is pointless for the crash; the real guard must be (and is) at the JS layer.
|
|
45
|
+
decisionHandler(WKPermissionDecision.Grant);
|
|
46
|
+
}
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
// Preserve the behavior NS's own UIDelegate gave us (we fully replace it):
|
|
49
|
+
// target=_blank / window.open has no new window in a shell → load it in place.
|
|
50
|
+
webViewCreateWebViewWithConfigurationForNavigationActionWindowFeatures(
|
|
51
|
+
webView: WKWebView,
|
|
52
|
+
_configuration: WKWebViewConfiguration,
|
|
53
|
+
navigationAction: WKNavigationAction,
|
|
54
|
+
_windowFeatures: WKWindowFeatures
|
|
55
|
+
): WKWebView {
|
|
56
|
+
const frame = navigationAction.targetFrame;
|
|
57
|
+
if (!frame || !frame.mainFrame) webView.loadRequest(navigationAction.request);
|
|
58
|
+
return null as unknown as WKWebView;
|
|
59
|
+
}
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
61
|
+
webViewRunJavaScriptAlertPanelWithMessageInitiatedByFrameCompletionHandler(
|
|
62
|
+
_webView: WKWebView,
|
|
63
|
+
message: string,
|
|
64
|
+
_frame: WKFrameInfo,
|
|
65
|
+
completionHandler: () => void
|
|
66
|
+
): void {
|
|
67
|
+
Utils.dispatchToMainThread(() => {
|
|
68
|
+
const alert = UIAlertController.alertControllerWithTitleMessagePreferredStyle(
|
|
69
|
+
null as unknown as string,
|
|
70
|
+
String(message ?? ''),
|
|
71
|
+
UIAlertControllerStyle.Alert
|
|
72
|
+
);
|
|
73
|
+
alert.addAction(
|
|
74
|
+
UIAlertAction.actionWithTitleStyleHandler('OK', UIAlertActionStyle.Default, () => completionHandler())
|
|
75
|
+
);
|
|
76
|
+
present(alert);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
71
79
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
80
|
+
webViewRunJavaScriptConfirmPanelWithMessageInitiatedByFrameCompletionHandler(
|
|
81
|
+
_webView: WKWebView,
|
|
82
|
+
message: string,
|
|
83
|
+
_frame: WKFrameInfo,
|
|
84
|
+
completionHandler: (ok: boolean) => void
|
|
85
|
+
): void {
|
|
86
|
+
Utils.dispatchToMainThread(() => {
|
|
87
|
+
const alert = UIAlertController.alertControllerWithTitleMessagePreferredStyle(
|
|
88
|
+
null as unknown as string,
|
|
89
|
+
String(message ?? ''),
|
|
90
|
+
UIAlertControllerStyle.Alert
|
|
91
|
+
);
|
|
92
|
+
alert.addAction(
|
|
93
|
+
UIAlertAction.actionWithTitleStyleHandler('Cancel', UIAlertActionStyle.Cancel, () =>
|
|
94
|
+
completionHandler(false)
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
alert.addAction(
|
|
98
|
+
UIAlertAction.actionWithTitleStyleHandler('OK', UIAlertActionStyle.Default, () => completionHandler(true))
|
|
99
|
+
);
|
|
100
|
+
present(alert);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
95
103
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
104
|
+
webViewRunJavaScriptTextInputPanelWithPromptDefaultTextInitiatedByFrameCompletionHandler(
|
|
105
|
+
_webView: WKWebView,
|
|
106
|
+
prompt: string,
|
|
107
|
+
defaultText: string,
|
|
108
|
+
_frame: WKFrameInfo,
|
|
109
|
+
completionHandler: (text: string) => void
|
|
110
|
+
): void {
|
|
111
|
+
Utils.dispatchToMainThread(() => {
|
|
112
|
+
const alert = UIAlertController.alertControllerWithTitleMessagePreferredStyle(
|
|
113
|
+
null as unknown as string,
|
|
114
|
+
String(prompt ?? ''),
|
|
115
|
+
UIAlertControllerStyle.Alert
|
|
116
|
+
);
|
|
117
|
+
let field: UITextField | null = null;
|
|
118
|
+
alert.addTextFieldWithConfigurationHandler((tf: UITextField) => {
|
|
119
|
+
tf.text = String(defaultText ?? '');
|
|
120
|
+
field = tf;
|
|
121
|
+
});
|
|
122
|
+
alert.addAction(
|
|
123
|
+
UIAlertAction.actionWithTitleStyleHandler('Cancel', UIAlertActionStyle.Cancel, () =>
|
|
124
|
+
completionHandler(null as unknown as string)
|
|
125
|
+
)
|
|
126
|
+
);
|
|
127
|
+
alert.addAction(
|
|
128
|
+
UIAlertAction.actionWithTitleStyleHandler('OK', UIAlertActionStyle.Default, () =>
|
|
129
|
+
completionHandler(field ? String(field.text ?? '') : '')
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
present(alert);
|
|
113
133
|
});
|
|
114
|
-
|
|
115
|
-
UIAlertAction.actionWithTitleStyleHandler('Cancel', UIAlertActionStyle.Cancel, () =>
|
|
116
|
-
completionHandler(null as unknown as string)
|
|
117
|
-
)
|
|
118
|
-
);
|
|
119
|
-
alert.addAction(
|
|
120
|
-
UIAlertAction.actionWithTitleStyleHandler('OK', UIAlertActionStyle.Default, () =>
|
|
121
|
-
completionHandler(field ? String(field.text ?? '') : '')
|
|
122
|
-
)
|
|
123
|
-
);
|
|
124
|
-
present(alert);
|
|
125
|
-
});
|
|
134
|
+
}
|
|
126
135
|
}
|
|
136
|
+
AppwrapUiDelegate = AppwrapUiDelegateImpl;
|
|
137
|
+
return AppwrapUiDelegate;
|
|
127
138
|
}
|
|
128
139
|
|
|
129
140
|
export function createUiDelegate(): WKUIDelegate {
|
|
130
|
-
return
|
|
141
|
+
return appwrapUiDelegateClass().new();
|
|
131
142
|
}
|
|
132
143
|
|
|
133
144
|
function present(alert: UIAlertController): void {
|
|
@@ -27,10 +27,14 @@ export function enableAndroidEdgeToEdge(): void {
|
|
|
27
27
|
if (!activity) return;
|
|
28
28
|
try {
|
|
29
29
|
// Explicit transparent status-bar colors (light + dark) so the page background — not the NS
|
|
30
|
-
// page backgroundColor — shows under the status bar.
|
|
30
|
+
// page backgroundColor — shows under the status bar. In FULL edge-to-edge mode (the edgeToEdge
|
|
31
|
+
// config + wireAndroidSafeArea un-boxing) also make the NAV bar transparent so the web content
|
|
32
|
+
// draws under it too; otherwise keep NS's default nav-bar scrim (so the buttons stay legible on
|
|
33
|
+
// apps that aren't drawing their own content down there).
|
|
31
34
|
Utils.android.enableEdgeToEdge(activity, {
|
|
32
35
|
statusBarLightColor: transparent,
|
|
33
36
|
statusBarDarkColor: transparent,
|
|
37
|
+
...(SHELL_CONFIG.edgeToEdge ? { navigationBarLightColor: transparent, navigationBarDarkColor: transparent } : {}),
|
|
34
38
|
});
|
|
35
39
|
} catch (e) {
|
|
36
40
|
console.warn('AppWrap: edge-to-edge setup failed', e);
|
|
@@ -52,11 +56,39 @@ export function enableAndroidEdgeToEdge(): void {
|
|
|
52
56
|
*/
|
|
53
57
|
export function wireAndroidSafeArea(webView: View): void {
|
|
54
58
|
if (!isAndroid || !SHELL_CONFIG.edgeToEdge) return;
|
|
55
|
-
// Un-box the
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
(
|
|
59
|
+
// Un-box the WebView so it fills the window UNDER the system bars. 'dont-apply' = NS won't pad the
|
|
60
|
+
// view with the system-bar insets. The insets are consumed/applied by the root FRAME (NS sets its
|
|
61
|
+
// androidOverflowEdge to 'ignore' by default — ui/frame/index.android.ts), NOT by the WebView's
|
|
62
|
+
// immediate parent, so overriding only webView.parent left the content boxed. Set it up the whole
|
|
63
|
+
// chain — root view (the Frame), the Page, and the WebView's parent — so whichever applies the
|
|
64
|
+
// insets stops padding. (androidOverflowEdge isn't in the public View typings — an NS layout hint.)
|
|
65
|
+
const targets: Array<View | null> = [
|
|
66
|
+
Application.getRootView() as View | null,
|
|
67
|
+
currentPage,
|
|
68
|
+
(webView.parent as View) ?? null,
|
|
69
|
+
webView,
|
|
70
|
+
];
|
|
71
|
+
for (const t of targets) if (t) (t as any).androidOverflowEdge = 'dont-apply';
|
|
72
|
+
|
|
73
|
+
// HIDE the navigation bar (immersive) so the game is genuinely full-bleed at the bottom — drawing
|
|
74
|
+
// under a nav bar you can still see is moot. Status bar stays visible (clock/battery over the page).
|
|
75
|
+
// BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE: a swipe from the bottom edge reveals it briefly, then it
|
|
76
|
+
// re-hides — the standard game pattern. Immersive resets on resume / when transiently shown, so it's
|
|
77
|
+
// re-applied below. API 30+ (WindowInsetsController); the device floor for edge-to-edge is well above.
|
|
78
|
+
const hideNav = () => {
|
|
79
|
+
try {
|
|
80
|
+
const controller = Application.android?.startActivity?.getWindow()?.getInsetsController?.();
|
|
81
|
+
if (!controller) return;
|
|
82
|
+
controller.hide(android.view.WindowInsets.Type.navigationBars());
|
|
83
|
+
controller.setSystemBarsBehavior(
|
|
84
|
+
android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
|
85
|
+
);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.warn('AppWrap: hide nav bar failed', e);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
hideNav();
|
|
91
|
+
setTimeout(hideNav, 60); // win the race against NS's own post-load bar setup
|
|
60
92
|
|
|
61
93
|
const density = Screen.mainScreen.scale || 1; // physical px per dp
|
|
62
94
|
const inject = (l: number, t: number, r: number, b: number) => {
|
|
@@ -102,8 +134,8 @@ export function wireAndroidSafeArea(webView: View): void {
|
|
|
102
134
|
setTimeout(tick, 150);
|
|
103
135
|
};
|
|
104
136
|
setTimeout(tick, 80);
|
|
105
|
-
Application.on(Application.resumeEvent, () => setTimeout(readAndInject, 120));
|
|
106
|
-
Application.on(Application.orientationChangedEvent, () => setTimeout(readAndInject, 200));
|
|
137
|
+
Application.on(Application.resumeEvent, () => { setTimeout(readAndInject, 120); setTimeout(hideNav, 120); });
|
|
138
|
+
Application.on(Application.orientationChangedEvent, () => { setTimeout(readAndInject, 200); setTimeout(hideNav, 200); });
|
|
107
139
|
}
|
|
108
140
|
|
|
109
141
|
export function bindStatusBarPage(page: Page): void {
|
package/src/cli.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type * as CapManifest from '../../../runtime/app/shell/capabilities.manif
|
|
|
20
20
|
// Config shape lives in its own import-safe module so a `appwrap.config.ts` file can import the
|
|
21
21
|
// type + `defineConfig` helper without pulling in (and running) the CLI dispatch.
|
|
22
22
|
import type { AppwrapConfig } from './config';
|
|
23
|
+
import { unknownConfigKeys } from './config';
|
|
23
24
|
import {
|
|
24
25
|
androidScreenOrientation,
|
|
25
26
|
applyBuildNumberFlag,
|
|
@@ -377,6 +378,15 @@ async function loadConfig(cwd: string, flags: Record<string, string>): Promise<A
|
|
|
377
378
|
}
|
|
378
379
|
const cfg = await readConfigFile(configPath);
|
|
379
380
|
|
|
381
|
+
// Warn (never fail) on keys this appwrap doesn't recognize. A config authored for a NEWER appwrap
|
|
382
|
+
// silently no-ops its unknown keys on an older install (e.g. `targetedDevices` before 0.39 → wrong
|
|
383
|
+
// device family, no error). Turn that silent no-op into a signal.
|
|
384
|
+
const stray = unknownConfigKeys(cfg as unknown as Record<string, unknown>);
|
|
385
|
+
if (stray.length) {
|
|
386
|
+
const ver = pkgVersion(resolve(import.meta.dir, '../package.json'));
|
|
387
|
+
for (const k of stray) console.warn(`⚠ appwrap: unrecognized config key '${k}' — ignored. If you expect it to apply, your installed @livx.cc/appwrap (${ver}) may predate it — upgrade.`);
|
|
388
|
+
}
|
|
389
|
+
|
|
380
390
|
// Manifest as source: the appwrap config wins, the PWA manifest fills the gaps, template default last.
|
|
381
391
|
// (DRY single-source — devs don't re-type identity already declared in the manifest.) See mergeManifest.
|
|
382
392
|
if (cfg.pwaDist) mergeManifest(cfg, loadManifest(cwd, cfg));
|
|
@@ -819,6 +829,15 @@ function generateIcons(cwd: string, outDir: string, cfg: AppwrapConfig): void {
|
|
|
819
829
|
console.warn('⚠ No app icon source found (manifest icons or config `icon`) — keeping template icons');
|
|
820
830
|
return;
|
|
821
831
|
}
|
|
832
|
+
// Icon rasterization uses `sips`, which is macOS-only. On Linux/CI (e.g. a GitHub Actions Android
|
|
833
|
+
// build) it's absent — skip gracefully and keep the template icons rather than throwing and failing
|
|
834
|
+
// the whole init/build. Regenerate real icons on macOS for store builds.
|
|
835
|
+
try {
|
|
836
|
+
execFileSync('which', ['sips'], { stdio: 'ignore' });
|
|
837
|
+
} catch {
|
|
838
|
+
console.warn('⚠ `sips` unavailable (non-macOS host) — keeping template icons; regenerate on macOS for store builds');
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
822
841
|
const probe = (prop: string) =>
|
|
823
842
|
parseInt(execFileSync('sips', ['-g', prop, source]).toString().match(/(\d+)\s*$/)?.[1] ?? '0', 10);
|
|
824
843
|
const w = probe('pixelWidth');
|
|
@@ -868,16 +887,36 @@ function generateIcons(cwd: string, outDir: string, cfg: AppwrapConfig): void {
|
|
|
868
887
|
|
|
869
888
|
/** Tint the iOS launch screen to the configured background color. */
|
|
870
889
|
function stampLaunchScreen(outDir: string, cfg: AppwrapConfig): void {
|
|
871
|
-
|
|
872
|
-
if (!existsSync(storyboard) || !cfg.backgroundColor) return;
|
|
890
|
+
if (!cfg.backgroundColor) return;
|
|
873
891
|
const hex = cfg.backgroundColor.replace('#', '');
|
|
874
892
|
if (!/^[0-9a-fA-F]{6}$/.test(hex)) return;
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
893
|
+
|
|
894
|
+
// iOS: tint the storyboard launch background.
|
|
895
|
+
const storyboard = join(outDir, 'App_Resources/iOS/LaunchScreen.storyboard');
|
|
896
|
+
if (existsSync(storyboard)) {
|
|
897
|
+
const ch = (i: number) => (parseInt(hex.slice(i, i + 2), 16) / 255).toFixed(4);
|
|
898
|
+
const src = readFileSync(storyboard, 'utf8').replace(
|
|
899
|
+
/<color key="backgroundColor"[^/]*\/>/g,
|
|
900
|
+
`<color key="backgroundColor" red="${ch(0)}" green="${ch(2)}" blue="${ch(4)}" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>`
|
|
901
|
+
);
|
|
902
|
+
writeFileSync(storyboard, src);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Android: replace the default NativeScript splash (background bitmap + NS logo) with a solid fill
|
|
906
|
+
// of the app's backgroundColor — parity with the iOS solid launch screen, and no NS branding flash.
|
|
907
|
+
const splash = join(outDir, 'App_Resources/Android/src/main/res/drawable-nodpi/splash_screen.xml');
|
|
908
|
+
if (existsSync(splash)) {
|
|
909
|
+
writeFileSync(splash,
|
|
910
|
+
`<?xml version="1.0" encoding="utf-8"?>\n` +
|
|
911
|
+
`<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:gravity="fill">\n` +
|
|
912
|
+
` <item>\n` +
|
|
913
|
+
` <shape android:shape="rectangle">\n` +
|
|
914
|
+
` <solid android:color="#${hex.toUpperCase()}" />\n` +
|
|
915
|
+
` </shape>\n` +
|
|
916
|
+
` </item>\n` +
|
|
917
|
+
`</layer-list>\n`
|
|
918
|
+
);
|
|
919
|
+
}
|
|
881
920
|
}
|
|
882
921
|
|
|
883
922
|
const VERSION_FILE = '.appwrap-version';
|
|
@@ -1201,11 +1240,33 @@ function runNs(outDir: string, args: string[]): void {
|
|
|
1201
1240
|
execFileSync('ns', args, { cwd: outDir, stdio: 'inherit' });
|
|
1202
1241
|
}
|
|
1203
1242
|
|
|
1243
|
+
/** Read the loader currently stamped into the generated shell (app/shell/config.ts). Used to
|
|
1244
|
+
* preserve an ACTIVE `dev` loader across `run` (see run()'s footgun note). Returns null if the
|
|
1245
|
+
* generated config is absent/unreadable — the caller then falls back to the appwrap config. */
|
|
1246
|
+
function readStampedLoader(outDir: string): { loader: string; serverUrl: string; debug: boolean } | null {
|
|
1247
|
+
try {
|
|
1248
|
+
const src = readFileSync(join(outDir, 'app/shell/config.ts'), 'utf8');
|
|
1249
|
+
const loader = src.match(/loader:\s*"([^"]*)"/)?.[1];
|
|
1250
|
+
if (!loader) return null;
|
|
1251
|
+
return {
|
|
1252
|
+
loader,
|
|
1253
|
+
serverUrl: src.match(/serverUrl:\s*"([^"]*)"/)?.[1] ?? '',
|
|
1254
|
+
debug: /debug:\s*true/.test(src),
|
|
1255
|
+
};
|
|
1256
|
+
} catch {
|
|
1257
|
+
return null;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1204
1261
|
/** `appwrap run <ios|android> [--device <id|name>]` — compile + boot the wrapper in a
|
|
1205
1262
|
* simulator/emulator (or named device) with live reload: the appwrap-managed replacement for raw
|
|
1206
|
-
* `ns run`, driven from the PWA project root with deps auto-installed.
|
|
1207
|
-
*
|
|
1208
|
-
*
|
|
1263
|
+
* `ns run`, driven from the PWA project root with deps auto-installed.
|
|
1264
|
+
*
|
|
1265
|
+
* Refreshes the generated wrapper from the source template + PWA first (same regenerateCore as
|
|
1266
|
+
* `sync`/`build`) so framework `runtime/` edits and PWA rebuilds actually reach the device — `run`
|
|
1267
|
+
* used to skip this and silently ship the STALE generated shell (the run-without-sync footgun). An
|
|
1268
|
+
* ACTIVE `dev` loader (loader:'server', stamped by `appwrap dev`) is preserved so dev→run
|
|
1269
|
+
* live-reload isn't clobbered back to the bundled loader. */
|
|
1209
1270
|
async function run(cwd: string, flags: Record<string, string>, positionals: string[]): Promise<void> {
|
|
1210
1271
|
const platform = positionals[0];
|
|
1211
1272
|
if (platform !== 'ios' && platform !== 'android') {
|
|
@@ -1217,6 +1278,17 @@ async function run(cwd: string, flags: Record<string, string>, positionals: stri
|
|
|
1217
1278
|
console.error(`✖ Wrapper not found at ${outDir} — run \`appwrap init\` first`);
|
|
1218
1279
|
process.exit(1);
|
|
1219
1280
|
}
|
|
1281
|
+
const cfg = await loadConfig(cwd, flags);
|
|
1282
|
+
const stamped = readStampedLoader(outDir);
|
|
1283
|
+
const devActive = stamped?.loader === 'server';
|
|
1284
|
+
// Preserve a live dev loader; otherwise regenerate from the appwrap config (bundled loader).
|
|
1285
|
+
const effectiveCfg = devActive
|
|
1286
|
+
? { ...cfg, loader: 'server' as const, serverUrl: stamped!.serverUrl, debug: stamped!.debug }
|
|
1287
|
+
: cfg;
|
|
1288
|
+
regenerateCore(cwd, outDir, effectiveCfg, { flags });
|
|
1289
|
+
applyOverrides(cwd, outDir, effectiveCfg); // overrides win last — same order as sync (else run wipes them)
|
|
1290
|
+
stampVersionManifest(outDir, effectiveCfg); // keep the managed-marker / provenance current
|
|
1291
|
+
console.log(devActive ? `✓ Refreshed wrapper (preserved dev loader → ${stamped!.serverUrl})` : '✓ Refreshed wrapper from template + PWA');
|
|
1220
1292
|
runNs(outDir, ['run', platform, ...(flags.device ? ['--device', flags.device] : [])]);
|
|
1221
1293
|
}
|
|
1222
1294
|
|
package/src/config.ts
CHANGED
|
@@ -200,3 +200,24 @@ export interface AppwrapConfig {
|
|
|
200
200
|
export function defineConfig(config: AppwrapConfig): AppwrapConfig {
|
|
201
201
|
return config;
|
|
202
202
|
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Top-level keys appwrap recognizes — KEEP IN SYNC with `AppwrapConfig` above (top-level only; nested
|
|
206
|
+
* keys like `push.ios` are not listed). Drives `unknownConfigKeys`, which warns (never fails) on stray
|
|
207
|
+
* keys at load time. The motivating bug: a config written for a NEWER appwrap silently no-ops its
|
|
208
|
+
* unknown keys on an OLDER installed version (e.g. `targetedDevices` before 0.39 → a universal build
|
|
209
|
+
* with no error, then an App Store rejection). A loud warning turns that silent no-op into a signal.
|
|
210
|
+
*/
|
|
211
|
+
export const KNOWN_CONFIG_KEYS: ReadonlySet<string> = new Set([
|
|
212
|
+
'appBoundDomains', 'backendOrigin', 'backgroundColor', 'backgroundTasks', 'buildNumber', 'debug',
|
|
213
|
+
'debugLog', 'devMenu', 'edgeToEdge', 'entry', 'icon', 'id', 'loader', 'modules', 'name',
|
|
214
|
+
'neutralizeServiceWorker', 'openNewWindowsInBrowser', 'orientation', 'overrides', 'permissions',
|
|
215
|
+
'plugins', 'push', 'pwaDist', 'queryPackages', 'queryUrlSchemes', 'serverUrl', 'statusBarStyle',
|
|
216
|
+
'storekitConfig', 'targetedDevices', 'teamId', 'themeColor', 'trackingDomains', 'urlScheme',
|
|
217
|
+
'usesNonExemptEncryption', 'vendorPaths', 'version',
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
/** Config keys appwrap doesn't recognize (pure — the caller decides how to surface them). */
|
|
221
|
+
export function unknownConfigKeys(cfg: Record<string, unknown>): string[] {
|
|
222
|
+
return Object.keys(cfg).filter((k) => !KNOWN_CONFIG_KEYS.has(k));
|
|
223
|
+
}
|