@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
|
@@ -12,118 +12,122 @@ import { Utils } from '@nativescript/core';
|
|
|
12
12
|
*
|
|
13
13
|
* Returns a retained delegate instance (the WKWebView holds uiDelegate weakly).
|
|
14
14
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
_origin: any,
|
|
22
|
-
_frame: any,
|
|
23
|
-
_type: any,
|
|
24
|
-
decisionHandler: (decision: number) => void
|
|
25
|
-
): void {
|
|
26
|
-
// WKPermissionDecision.Grant === 1 — silently grant (the app already holds the
|
|
27
|
-
// OS-level mic/camera permission; media.ensurePermission established TCC).
|
|
28
|
-
// NOTE: this only ever sees capabilities the build DECLARED — a request for an
|
|
29
|
-
// UNDECLARED capability is rejected upstream in JS (mediaCaptureGuardJs), because
|
|
30
|
-
// WebKit asks TCC to authorize the device AROUND this callback and a missing usage
|
|
31
|
-
// string is a hard process-kill a native `deny` here cannot prevent. So denying
|
|
32
|
-
// here is pointless for the crash; the real guard must be (and is) at the JS layer.
|
|
33
|
-
decisionHandler(1);
|
|
34
|
-
},
|
|
15
|
+
@NativeClass()
|
|
16
|
+
class AppwrapUiDelegate extends NSObject implements WKUIDelegate {
|
|
17
|
+
static ObjCProtocols = [WKUIDelegate];
|
|
18
|
+
static new(): AppwrapUiDelegate {
|
|
19
|
+
return <AppwrapUiDelegate>super.new();
|
|
20
|
+
}
|
|
35
21
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
22
|
+
// iOS 15+: WKUIDelegate media capture permission.
|
|
23
|
+
webViewRequestMediaCapturePermissionForOriginInitiatedByFrameTypeDecisionHandler(
|
|
24
|
+
_webView: WKWebView,
|
|
25
|
+
_origin: WKSecurityOrigin,
|
|
26
|
+
_frame: WKFrameInfo,
|
|
27
|
+
_type: WKMediaCaptureType,
|
|
28
|
+
decisionHandler: (decision: WKPermissionDecision) => void
|
|
29
|
+
): void {
|
|
30
|
+
// WKPermissionDecision.Grant === 1 — silently grant (the app already holds the
|
|
31
|
+
// OS-level mic/camera permission; media.ensurePermission established TCC).
|
|
32
|
+
// NOTE: this only ever sees capabilities the build DECLARED — a request for an
|
|
33
|
+
// UNDECLARED capability is rejected upstream in JS (mediaCaptureGuardJs), because
|
|
34
|
+
// WebKit asks TCC to authorize the device AROUND this callback and a missing usage
|
|
35
|
+
// string is a hard process-kill a native `deny` here cannot prevent. So denying
|
|
36
|
+
// here is pointless for the crash; the real guard must be (and is) at the JS layer.
|
|
37
|
+
decisionHandler(WKPermissionDecision.Grant);
|
|
38
|
+
}
|
|
48
39
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
alert.addAction(
|
|
62
|
-
UIAlertAction.actionWithTitleStyleHandler('OK', UIAlertActionStyle.Default, () => completionHandler())
|
|
63
|
-
);
|
|
64
|
-
present(alert);
|
|
65
|
-
});
|
|
66
|
-
},
|
|
40
|
+
// Preserve the behavior NS's own UIDelegate gave us (we fully replace it):
|
|
41
|
+
// target=_blank / window.open has no new window in a shell → load it in place.
|
|
42
|
+
webViewCreateWebViewWithConfigurationForNavigationActionWindowFeatures(
|
|
43
|
+
webView: WKWebView,
|
|
44
|
+
_configuration: WKWebViewConfiguration,
|
|
45
|
+
navigationAction: WKNavigationAction,
|
|
46
|
+
_windowFeatures: WKWindowFeatures
|
|
47
|
+
): WKWebView {
|
|
48
|
+
const frame = navigationAction.targetFrame;
|
|
49
|
+
if (!frame || !frame.mainFrame) webView.loadRequest(navigationAction.request);
|
|
50
|
+
return null as unknown as WKWebView;
|
|
51
|
+
}
|
|
67
52
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
UIAlertAction.actionWithTitleStyleHandler('OK', UIAlertActionStyle.Default, () => completionHandler(true))
|
|
87
|
-
);
|
|
88
|
-
present(alert);
|
|
89
|
-
});
|
|
90
|
-
},
|
|
53
|
+
webViewRunJavaScriptAlertPanelWithMessageInitiatedByFrameCompletionHandler(
|
|
54
|
+
_webView: WKWebView,
|
|
55
|
+
message: string,
|
|
56
|
+
_frame: WKFrameInfo,
|
|
57
|
+
completionHandler: () => void
|
|
58
|
+
): void {
|
|
59
|
+
Utils.dispatchToMainThread(() => {
|
|
60
|
+
const alert = UIAlertController.alertControllerWithTitleMessagePreferredStyle(
|
|
61
|
+
null as unknown as string,
|
|
62
|
+
String(message ?? ''),
|
|
63
|
+
UIAlertControllerStyle.Alert
|
|
64
|
+
);
|
|
65
|
+
alert.addAction(
|
|
66
|
+
UIAlertAction.actionWithTitleStyleHandler('OK', UIAlertActionStyle.Default, () => completionHandler())
|
|
67
|
+
);
|
|
68
|
+
present(alert);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
91
71
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
72
|
+
webViewRunJavaScriptConfirmPanelWithMessageInitiatedByFrameCompletionHandler(
|
|
73
|
+
_webView: WKWebView,
|
|
74
|
+
message: string,
|
|
75
|
+
_frame: WKFrameInfo,
|
|
76
|
+
completionHandler: (ok: boolean) => void
|
|
77
|
+
): void {
|
|
78
|
+
Utils.dispatchToMainThread(() => {
|
|
79
|
+
const alert = UIAlertController.alertControllerWithTitleMessagePreferredStyle(
|
|
80
|
+
null as unknown as string,
|
|
81
|
+
String(message ?? ''),
|
|
82
|
+
UIAlertControllerStyle.Alert
|
|
83
|
+
);
|
|
84
|
+
alert.addAction(
|
|
85
|
+
UIAlertAction.actionWithTitleStyleHandler('Cancel', UIAlertActionStyle.Cancel, () =>
|
|
86
|
+
completionHandler(false)
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
alert.addAction(
|
|
90
|
+
UIAlertAction.actionWithTitleStyleHandler('OK', UIAlertActionStyle.Default, () => completionHandler(true))
|
|
91
|
+
);
|
|
92
|
+
present(alert);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
webViewRunJavaScriptTextInputPanelWithPromptDefaultTextInitiatedByFrameCompletionHandler(
|
|
97
|
+
_webView: WKWebView,
|
|
98
|
+
prompt: string,
|
|
99
|
+
defaultText: string,
|
|
100
|
+
_frame: WKFrameInfo,
|
|
101
|
+
completionHandler: (text: string) => void
|
|
102
|
+
): void {
|
|
103
|
+
Utils.dispatchToMainThread(() => {
|
|
104
|
+
const alert = UIAlertController.alertControllerWithTitleMessagePreferredStyle(
|
|
105
|
+
null as unknown as string,
|
|
106
|
+
String(prompt ?? ''),
|
|
107
|
+
UIAlertControllerStyle.Alert
|
|
108
|
+
);
|
|
109
|
+
let field: UITextField | null = null;
|
|
110
|
+
alert.addTextFieldWithConfigurationHandler((tf: UITextField) => {
|
|
111
|
+
tf.text = String(defaultText ?? '');
|
|
112
|
+
field = tf;
|
|
113
|
+
});
|
|
114
|
+
alert.addAction(
|
|
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
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function createUiDelegate(): WKUIDelegate {
|
|
130
|
+
return AppwrapUiDelegate.new();
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
function present(alert: UIAlertController): void {
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { knownFolders, path as nsPath } from '@nativescript/core';
|
|
2
2
|
|
|
3
|
-
declare const NSString: any;
|
|
4
|
-
|
|
5
3
|
/**
|
|
6
4
|
* Single debug log sink → Documents/appwrap-web.log. Both the forwarded WebView console
|
|
7
5
|
* (custom-webview) and native shell diagnostics (handlers) write here, because NativeScript's
|
|
@@ -15,7 +13,7 @@ export function appwrapNativeLog(line: string): void {
|
|
|
15
13
|
_log += line + '\n';
|
|
16
14
|
if (_log.length > 160000) _log = _log.slice(-120000);
|
|
17
15
|
const p = nsPath.join(knownFolders.documents().path, 'appwrap-web.log');
|
|
18
|
-
NSString.stringWithString(_log).writeToFileAtomicallyEncodingError(p, true, 4 /*NSUTF8
|
|
16
|
+
NSString.stringWithString(_log).writeToFileAtomicallyEncodingError(p, true, 4 /*NSUTF8*/);
|
|
19
17
|
} catch {
|
|
20
18
|
/* best-effort */
|
|
21
19
|
}
|
|
@@ -28,7 +28,7 @@ export function enableAndroidEdgeToEdge(): void {
|
|
|
28
28
|
try {
|
|
29
29
|
// Explicit transparent status-bar colors (light + dark) so the page background — not the NS
|
|
30
30
|
// page backgroundColor — shows under the status bar. Nav bar keeps NS's default subtle scrim.
|
|
31
|
-
|
|
31
|
+
Utils.android.enableEdgeToEdge(activity, {
|
|
32
32
|
statusBarLightColor: transparent,
|
|
33
33
|
statusBarDarkColor: transparent,
|
|
34
34
|
});
|
|
@@ -55,6 +55,7 @@ export function wireAndroidSafeArea(webView: View): void {
|
|
|
55
55
|
// Un-box the layout that wraps the WebView so it (and its child WebView) fills the window UNDER the
|
|
56
56
|
// bars. 'dont-apply' = NS won't pad it with the system-bar insets.
|
|
57
57
|
const layout = (webView.parent as View) ?? webView;
|
|
58
|
+
// androidOverflowEdge: not in the public View typings — an NS edge-to-edge layout hint set dynamically.
|
|
58
59
|
(layout as any).androidOverflowEdge = 'dont-apply';
|
|
59
60
|
|
|
60
61
|
const density = Screen.mainScreen.scale || 1; // physical px per dp
|
|
@@ -65,7 +66,8 @@ export function wireAndroidSafeArea(webView: View): void {
|
|
|
65
66
|
`s.setProperty('--saie-right','${r}px');s.setProperty('--saie-bottom','${b}px');` +
|
|
66
67
|
`})(document.documentElement.style);`;
|
|
67
68
|
try {
|
|
68
|
-
|
|
69
|
+
// webView.android is typed `any` by NS core; on Android it's an android.webkit.WebView.
|
|
70
|
+
webView.android?.evaluateJavascript(js, null);
|
|
69
71
|
} catch (e) {
|
|
70
72
|
console.warn('AppWrap: safe-area inject failed', e);
|
|
71
73
|
}
|
|
@@ -82,7 +84,7 @@ export function wireAndroidSafeArea(webView: View): void {
|
|
|
82
84
|
const decor = Application.android?.startActivity?.getWindow()?.getDecorView();
|
|
83
85
|
const wi = decor?.getRootWindowInsets?.();
|
|
84
86
|
if (!wi) return false;
|
|
85
|
-
const Type =
|
|
87
|
+
const Type = android.view.WindowInsets.Type;
|
|
86
88
|
const ins = wi.getInsets(Type.systemBars() | Type.displayCutout());
|
|
87
89
|
inject(dp(ins.left), dp(ins.top), dp(ins.right), dp(ins.bottom));
|
|
88
90
|
return ins.top + ins.bottom + ins.left + ins.right > 0;
|
|
@@ -132,12 +134,13 @@ export function applyThemeColor(color: string): void {
|
|
|
132
134
|
/** 'light' = white icons/text (for dark backgrounds), 'dark' = black. */
|
|
133
135
|
export function setStatusBarStyle(style: 'light' | 'dark'): void {
|
|
134
136
|
if (isIOS) {
|
|
137
|
+
// statusBarStyle is a CSS-backed Page property, not on the public Page type — set dynamically.
|
|
135
138
|
if (currentPage) (currentPage as any).statusBarStyle = style;
|
|
136
139
|
} else if (isAndroid) {
|
|
137
140
|
const window = Application.android?.startActivity?.getWindow();
|
|
138
141
|
if (!window) return;
|
|
139
142
|
const decorView = window.getDecorView();
|
|
140
|
-
const controller =
|
|
143
|
+
const controller = decorView.getWindowInsetsController?.();
|
|
141
144
|
if (controller) {
|
|
142
145
|
const APPEARANCE_LIGHT_STATUS_BARS = 8; // WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
|
|
143
146
|
// Android "light status bar" = dark icons; invert from our naming
|
|
@@ -4,7 +4,7 @@ import { Utils, isAndroid, isIOS } from '@nativescript/core';
|
|
|
4
4
|
export function showToast(message: string, duration: 'short' | 'long' = 'short'): void {
|
|
5
5
|
const ms = duration === 'long' ? 3500 : 2000;
|
|
6
6
|
if (isAndroid) {
|
|
7
|
-
const Toast =
|
|
7
|
+
const Toast = android.widget.Toast;
|
|
8
8
|
Toast.makeText(
|
|
9
9
|
Utils.android.getApplicationContext(),
|
|
10
10
|
message,
|
package/runtime/references.d.ts
CHANGED
|
@@ -1,2 +1,23 @@
|
|
|
1
1
|
/// <reference path="./node_modules/@nativescript/types-ios/index.d.ts" />
|
|
2
2
|
/// <reference path="./node_modules/@nativescript/types-android/lib/android-32.d.ts" />
|
|
3
|
+
|
|
4
|
+
// Opt-in iOS framework typings the shell's native handlers use. @nativescript/types-ios ships the FULL
|
|
5
|
+
// SDK but its default index references only a curated subset (for IDE/compile speed); reference the
|
|
6
|
+
// extras here so handlers get real types + autocomplete instead of `any`. (NativeScript-documented:
|
|
7
|
+
// blog.nativescript.org/where-did-my-types-go) Path arch ('objc-x86_64') is where they were generated;
|
|
8
|
+
// the API surface is arch-independent for type-checking.
|
|
9
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!Contacts.d.ts" />
|
|
10
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!ContactsUI.d.ts" />
|
|
11
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!HealthKit.d.ts" />
|
|
12
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!EventKit.d.ts" />
|
|
13
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!CoreMotion.d.ts" />
|
|
14
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!StoreKit.d.ts" />
|
|
15
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!LocalAuthentication.d.ts" />
|
|
16
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!PhotosUI.d.ts" />
|
|
17
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!_LocationEssentials.d.ts" />
|
|
18
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!CoreLocation.d.ts" />
|
|
19
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!AVFoundation.d.ts" />
|
|
20
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!Speech.d.ts" />
|
|
21
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!BackgroundTasks.d.ts" />
|
|
22
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!AuthenticationServices.d.ts" />
|
|
23
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!Photos.d.ts" />
|
package/runtime/tsconfig.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -25,8 +25,25 @@ import {
|
|
|
25
25
|
iosOrientations,
|
|
26
26
|
mergeManifest,
|
|
27
27
|
stampAndroidOrientation,
|
|
28
|
+
stampAndroidQueries,
|
|
29
|
+
stampPlistBackgroundTasks,
|
|
28
30
|
stampPlistOrientations,
|
|
29
31
|
} from './derive';
|
|
32
|
+
import type { WebManifest } from './derive';
|
|
33
|
+
|
|
34
|
+
/** What `child_process.execFileSync` attaches to the Error it throws on a non-zero exit
|
|
35
|
+
* (stdout/stderr are Buffer with the default encoding, string when `encoding` is set). */
|
|
36
|
+
interface ExecError extends Error {
|
|
37
|
+
stdout?: string | Buffer;
|
|
38
|
+
stderr?: string | Buffer;
|
|
39
|
+
status?: number | null;
|
|
40
|
+
}
|
|
41
|
+
const asExecError = (e: unknown): ExecError => (e ?? {}) as ExecError;
|
|
42
|
+
/** Combined stdout+stderr captured on an exec failure (empty string when none). */
|
|
43
|
+
const execErrText = (e: unknown): string => {
|
|
44
|
+
const err = asExecError(e);
|
|
45
|
+
return `${err.stdout ?? ''}${err.stderr ?? ''}`;
|
|
46
|
+
};
|
|
30
47
|
|
|
31
48
|
/** Marketing version → a monotonic integer build (0.2.1 → 201; 1.4.12 → 10412). Stable & increasing
|
|
32
49
|
* across semver bumps so store re-uploads are always accepted without a manual bump. */
|
|
@@ -175,6 +192,7 @@ const OPTIONAL_GROUP_HANDLERS: Record<string, { file: string; fn: string }> = {
|
|
|
175
192
|
reviews: { file: './handlers-reviews', fn: 'registerReviewsHandlers' },
|
|
176
193
|
scanner: { file: './handlers-scanner', fn: 'registerScannerHandlers' },
|
|
177
194
|
speech: { file: './handlers-speech', fn: 'registerSpeechHandlers' },
|
|
195
|
+
backgroundTask: { file: './handlers-background', fn: 'registerBackgroundTaskHandlers' },
|
|
178
196
|
};
|
|
179
197
|
|
|
180
198
|
/** Generate the two composition artifacts in the wrapper: the active capability list (drives the
|
|
@@ -195,6 +213,18 @@ function generateModuleArtifacts(outDir: string, req: NativeReqs): void {
|
|
|
195
213
|
`/** Generated by \`appwrap\` — only the active strippable modules are imported. Do not edit. */\n` +
|
|
196
214
|
`${imports}${imports ? '\n' : ''}\nexport function registerOptionalHandlers(): void {\n${calls}\n}\n`
|
|
197
215
|
);
|
|
216
|
+
|
|
217
|
+
// iOS BGTaskScheduler launch handlers must register at didFinishLaunching (the AppDelegate calls
|
|
218
|
+
// registerBackgroundLaunchHandlers) — too early for the page-load barrel. Wire the real impl ONLY
|
|
219
|
+
// when backgroundTask is active, so a build without it never references BGTaskScheduler. No-op default.
|
|
220
|
+
const bgActive = req.activeOptionalGroups.includes('backgroundTask');
|
|
221
|
+
writeFileSync(
|
|
222
|
+
join(shell, 'background-bootstrap.generated.ts'),
|
|
223
|
+
`/** Generated by \`appwrap\` — wires the iOS BGTask launch handlers only when backgroundTask is active. Do not edit. */\n` +
|
|
224
|
+
(bgActive
|
|
225
|
+
? `export { registerBackgroundTaskLaunchHandlers as registerBackgroundLaunchHandlers } from './handlers-background';\n`
|
|
226
|
+
: `export function registerBackgroundLaunchHandlers(): void {}\n`)
|
|
227
|
+
);
|
|
198
228
|
}
|
|
199
229
|
|
|
200
230
|
/** Stamp the active modules' gradle dependencies into Android app.gradle. Idempotent marker block. */
|
|
@@ -286,15 +316,15 @@ function parseArgs(argv: string[]) {
|
|
|
286
316
|
}
|
|
287
317
|
|
|
288
318
|
/** Parse the PWA's web manifest (manifest.json / .webmanifest) from the dist dir, or null. */
|
|
289
|
-
function loadManifest(cwd: string, cfg: AppwrapConfig):
|
|
319
|
+
function loadManifest(cwd: string, cfg: AppwrapConfig): WebManifest | null {
|
|
290
320
|
const dist = resolve(cwd, cfg.pwaDist);
|
|
291
321
|
for (const name of ['manifest.json', 'manifest.webmanifest']) {
|
|
292
322
|
const mf = join(dist, name);
|
|
293
323
|
if (!existsSync(mf)) continue;
|
|
294
324
|
try {
|
|
295
325
|
return JSON.parse(readFileSync(mf, 'utf8'));
|
|
296
|
-
} catch (e:
|
|
297
|
-
console.warn(`⚠ Could not parse ${name}: ${e.message}`);
|
|
326
|
+
} catch (e: unknown) {
|
|
327
|
+
console.warn(`⚠ Could not parse ${name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
298
328
|
}
|
|
299
329
|
}
|
|
300
330
|
return null;
|
|
@@ -398,6 +428,12 @@ function stampIOSDisplayName(outDir: string, cfg: AppwrapConfig, req: NativeReqs
|
|
|
398
428
|
// arrays (iPhone + ~ipad). Skipped when unset → keep the template's free-rotation default.
|
|
399
429
|
if (cfg.orientation) src = stampPlistOrientations(src, iosOrientations(cfg.orientation));
|
|
400
430
|
|
|
431
|
+
// Headless background tasks (backgroundTask module): stamp BGTaskSchedulerPermittedIdentifiers +
|
|
432
|
+
// fetch/processing background modes from `backgroundTasks`. Idempotent both ways — passing []/undefined
|
|
433
|
+
// strips the block — so it no-ops (and cleans up) when the module is inactive or the field is absent.
|
|
434
|
+
const bgActive = req.activeOptionalGroups.includes('backgroundTask');
|
|
435
|
+
src = stampPlistBackgroundTasks(src, bgActive ? cfg.backgroundTasks : undefined);
|
|
436
|
+
|
|
401
437
|
// Permission usage strings + URL scheme + export-compliance — idempotent: strip stamped block, re-add
|
|
402
438
|
src = src.replace(/\s*<!-- appwrap:begin -->[\s\S]*?<!-- appwrap:end -->/g, '');
|
|
403
439
|
const extras: string[] = [];
|
|
@@ -412,6 +448,12 @@ function stampIOSDisplayName(outDir: string, cfg: AppwrapConfig, req: NativeReqs
|
|
|
412
448
|
` <key>CFBundleURLTypes</key>\n <array>\n <dict>\n <key>CFBundleTypeRole</key>\n <string>Editor</string>\n <key>CFBundleURLName</key>\n <string>${cfg.id}</string>\n <key>CFBundleURLSchemes</key>\n <array>\n <string>${cfg.urlScheme}</string>\n </array>\n </dict>\n </array>`
|
|
413
449
|
);
|
|
414
450
|
}
|
|
451
|
+
// Schemes kit.app.canOpenUrl() may probe → LSApplicationQueriesSchemes (iOS 9+ requires declaration
|
|
452
|
+
// for custom schemes). No-op when absent.
|
|
453
|
+
if (cfg.queryUrlSchemes?.length) {
|
|
454
|
+
const items = cfg.queryUrlSchemes.map((s) => ` <string>${s}</string>`).join('\n');
|
|
455
|
+
extras.push(` <key>LSApplicationQueriesSchemes</key>\n <array>\n${items}\n </array>`);
|
|
456
|
+
}
|
|
415
457
|
if (extras.length) {
|
|
416
458
|
src = src.replace(
|
|
417
459
|
/<\/dict>\s*<\/plist>\s*$/,
|
|
@@ -644,6 +686,10 @@ function stampAndroidAppName(outDir: string, cfg: AppwrapConfig, req: NativeReqs
|
|
|
644
686
|
/<!-- appwrap:application -->[\s\S]*?<!-- \/appwrap:application -->/,
|
|
645
687
|
`<!-- appwrap:application -->\n\t\t${req.androidManifestApp.join('\n\t\t')}\n\t\t<!-- /appwrap:application -->`
|
|
646
688
|
);
|
|
689
|
+
// <queries> for kit.app.canOpenUrl() visibility probes (API 30+) — idempotent marker. queryPackages
|
|
690
|
+
// → explicit <package>; queryUrlSchemes → a VIEW <intent> per scheme (symmetric with iOS's
|
|
691
|
+
// LSApplicationQueriesSchemes). See stampAndroidQueries.
|
|
692
|
+
src = stampAndroidQueries(src, cfg.queryPackages, cfg.queryUrlSchemes);
|
|
647
693
|
writeFileSync(manifest, src);
|
|
648
694
|
}
|
|
649
695
|
}
|
|
@@ -845,13 +891,13 @@ function vendorBackendAssets(www: string, cfg: AppwrapConfig): void {
|
|
|
845
891
|
// Retry a couple times (the backend can briefly reset under deploy/cold-start), and on total
|
|
846
892
|
// failure fall back to the cached copy if one exists rather than breaking the whole sync.
|
|
847
893
|
let ok = false;
|
|
848
|
-
let lastErr:
|
|
894
|
+
let lastErr: unknown;
|
|
849
895
|
for (let attempt = 0; attempt < 3 && !ok; attempt++) {
|
|
850
896
|
try {
|
|
851
897
|
execFileSync('curl', ['-fsSL', '--retry', '2', url, '-o', tmp], { stdio: 'pipe' });
|
|
852
898
|
cpSync(tmp, dest);
|
|
853
899
|
ok = true;
|
|
854
|
-
} catch (e:
|
|
900
|
+
} catch (e: unknown) {
|
|
855
901
|
lastErr = e;
|
|
856
902
|
}
|
|
857
903
|
}
|
|
@@ -860,10 +906,12 @@ function vendorBackendAssets(www: string, cfg: AppwrapConfig): void {
|
|
|
860
906
|
console.log(` vendor ← ${url}`);
|
|
861
907
|
} else if (existsSync(dest) && readFileSync(dest).length > 0) {
|
|
862
908
|
console.warn(`⚠ vendor fetch failed: ${url} — using cached copy (backend unreachable).`);
|
|
863
|
-
|
|
909
|
+
const err = execErrText(lastErr);
|
|
910
|
+
if (err) console.warn(` ${err.trim()}`);
|
|
864
911
|
} else {
|
|
865
912
|
console.error(`✖ vendor fetch failed: ${url} (backend reachable? path correct?) — no cached copy to fall back to`);
|
|
866
|
-
|
|
913
|
+
const err = execErrText(lastErr);
|
|
914
|
+
if (err) console.error(err.trim());
|
|
867
915
|
process.exit(1);
|
|
868
916
|
}
|
|
869
917
|
}
|
|
@@ -1090,19 +1138,27 @@ async function build(cwd: string, flags: Record<string, string>, positionals: st
|
|
|
1090
1138
|
|
|
1091
1139
|
interface DeviceInfo { id: string; name: string; model: string; transport: string }
|
|
1092
1140
|
|
|
1141
|
+
/** The subset of a `xcrun devicectl list devices --json-output` device entry appwrap reads. */
|
|
1142
|
+
interface DevicectlDevice {
|
|
1143
|
+
identifier?: string;
|
|
1144
|
+
deviceProperties?: { name?: string };
|
|
1145
|
+
hardwareProperties?: { platform?: string; marketingName?: string; productType?: string };
|
|
1146
|
+
connectionProperties?: { tunnelState?: string; transportType?: string };
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1093
1149
|
/** Discover usable physical iOS devices via devicectl (USB + network). Excludes 'unavailable'
|
|
1094
1150
|
* tunnels and non-iOS (watch). Returns [] if none. */
|
|
1095
1151
|
function listIosDevices(): DeviceInfo[] {
|
|
1096
1152
|
const out = join(tmpdir(), `appwrap-devices-${process.pid}.json`);
|
|
1097
1153
|
try {
|
|
1098
1154
|
execFileSync('xcrun', ['devicectl', 'list', 'devices', '--json-output', out], { stdio: 'pipe' });
|
|
1099
|
-
const j = JSON.parse(readFileSync(out, 'utf8'));
|
|
1155
|
+
const j = JSON.parse(readFileSync(out, 'utf8')) as { result?: { devices?: DevicectlDevice[] } };
|
|
1100
1156
|
rmSync(out, { force: true });
|
|
1101
1157
|
return (j?.result?.devices ?? [])
|
|
1102
|
-
.filter((d
|
|
1158
|
+
.filter((d) => d?.hardwareProperties?.platform === 'iOS'
|
|
1103
1159
|
&& d?.connectionProperties?.tunnelState !== 'unavailable')
|
|
1104
|
-
.map((d
|
|
1105
|
-
id: d.identifier,
|
|
1160
|
+
.map((d) => ({
|
|
1161
|
+
id: d.identifier ?? '',
|
|
1106
1162
|
name: d?.deviceProperties?.name ?? '(unknown)',
|
|
1107
1163
|
model: d?.hardwareProperties?.marketingName ?? d?.hardwareProperties?.productType ?? '',
|
|
1108
1164
|
transport: d?.connectionProperties?.transportType ?? '',
|
|
@@ -1129,7 +1185,7 @@ function pickDevice(devices: DeviceInfo[], explicitId?: string): DeviceInfo {
|
|
|
1129
1185
|
}
|
|
1130
1186
|
console.log('Multiple devices connected:');
|
|
1131
1187
|
devices.forEach((d, i) => console.log(` ${i + 1}) ${d.name} — ${d.model || 'iPhone'} [${d.transport}]`));
|
|
1132
|
-
const ans = (globalThis as
|
|
1188
|
+
const ans = (globalThis as { prompt(msg?: string): string | null }).prompt(`Select device [1-${devices.length}]: `);
|
|
1133
1189
|
const idx = Number(ans) - 1;
|
|
1134
1190
|
if (!Number.isInteger(idx) || idx < 0 || idx >= devices.length) {
|
|
1135
1191
|
console.error('✖ Invalid selection.'); process.exit(1);
|
|
@@ -1192,8 +1248,8 @@ async function deploy(cwd: string, flags: Record<string, string>, positionals: s
|
|
|
1192
1248
|
execFileSync('xcrun', ['devicectl', 'device', 'install', 'app', '--timeout', '25', '--device', device.id, ipaPath], { encoding: 'utf8', stdio: ['inherit', 'pipe', 'pipe'] })
|
|
1193
1249
|
);
|
|
1194
1250
|
process.stdout.write(out);
|
|
1195
|
-
} catch (e:
|
|
1196
|
-
const log =
|
|
1251
|
+
} catch (e: unknown) {
|
|
1252
|
+
const log = execErrText(e);
|
|
1197
1253
|
process.stderr.write(log);
|
|
1198
1254
|
if (/maximum number of installed apps|MIInstallerErrorDomain error 13|ApplicationVerificationFailed/.test(log)) {
|
|
1199
1255
|
// Free Apple developer profile caps a device at 3 app IDs — a sibling appwrap/WDA build often eats a slot.
|
|
@@ -1255,8 +1311,8 @@ function usbmuxInstall(ipaPath: string): boolean {
|
|
|
1255
1311
|
const out = execFileSync('ideviceinstaller', ['install', ipaPath], { encoding: 'utf8', stdio: ['inherit', 'pipe', 'pipe'] });
|
|
1256
1312
|
process.stdout.write(out);
|
|
1257
1313
|
return /Complete|Installed/i.test(out);
|
|
1258
|
-
} catch (e:
|
|
1259
|
-
process.stderr.write(
|
|
1314
|
+
} catch (e: unknown) {
|
|
1315
|
+
process.stderr.write(execErrText(e));
|
|
1260
1316
|
return false;
|
|
1261
1317
|
}
|
|
1262
1318
|
}
|
|
@@ -1269,8 +1325,8 @@ function withUnlockRetry<T>(label: string, run: () => T, tries = 40, delayMs = 3
|
|
|
1269
1325
|
for (let i = 0; ; i++) {
|
|
1270
1326
|
try {
|
|
1271
1327
|
return run();
|
|
1272
|
-
} catch (e:
|
|
1273
|
-
const log =
|
|
1328
|
+
} catch (e: unknown) {
|
|
1329
|
+
const log = execErrText(e);
|
|
1274
1330
|
if (/maximum number of installed apps|MIInstallerErrorDomain error 13|ApplicationVerificationFailed/.test(log)) throw e;
|
|
1275
1331
|
// devicectl/CoreDevice tunnel STUCK (it switches to a [wired] path and hangs) is NOT a lock —
|
|
1276
1332
|
// retrying as "waiting for unlock" is pointless + misleading. Re-throw so the caller can fall back.
|
package/src/config.ts
CHANGED
|
@@ -43,6 +43,19 @@ export interface AppwrapConfig {
|
|
|
43
43
|
pwaDist: string;
|
|
44
44
|
/** Custom URL scheme for deep links (e.g. "hellowrap" → hellowrap://...). */
|
|
45
45
|
urlScheme?: string;
|
|
46
|
+
/** Custom URL schemes `kit.app.canOpenUrl()` may probe (e.g. `['whatsapp', 'tg']`) — covers BOTH
|
|
47
|
+
* platforms with a single declaration. Since iOS 9 / Android API 30+ a probe of an undeclared custom
|
|
48
|
+
* scheme returns false for privacy. These stamp into iOS Info.plist `LSApplicationQueriesSchemes` AND
|
|
49
|
+
* Android `<queries>` as a VIEW `<intent>` per scheme — so scheme-based `canOpenUrl` is symmetric
|
|
50
|
+
* without hand-mapping each scheme to a package. Common schemes (http/https/tel/mailto/sms) need no
|
|
51
|
+
* declaration on either platform. No-op when absent. */
|
|
52
|
+
queryUrlSchemes?: string[];
|
|
53
|
+
/** Android-only explicit package visibility for `kit.app.canOpenUrl()` (e.g. `['com.whatsapp']`) —
|
|
54
|
+
* for probing a specific package directly. Under API 30+ package visibility, `resolveActivity`
|
|
55
|
+
* returns null for undeclared packages — these stamp into AndroidManifest `<queries>` as
|
|
56
|
+
* `<package android:name="..."/>`. For scheme probes prefer `queryUrlSchemes` (cross-platform).
|
|
57
|
+
* No-op when absent. */
|
|
58
|
+
queryPackages?: string[];
|
|
46
59
|
/** App icon source (≥512px square png). Defaults to the largest icon in the PWA manifest. */
|
|
47
60
|
icon?: string;
|
|
48
61
|
/** Loader: 'app' (default — app:// scheme, ES modules OK), 'file' (debug fallback), or 'server'
|
|
@@ -120,6 +133,13 @@ export interface AppwrapConfig {
|
|
|
120
133
|
* (the per-app `permissions{}` map only OVERRIDES the default usage copy). When ABSENT, every
|
|
121
134
|
* capability is active and permissions come solely from `permissions{}` (pre-modules behavior). */
|
|
122
135
|
modules?: string[];
|
|
136
|
+
/** Permitted headless background-task identifiers (for the `backgroundTask` module). iOS REQUIRES
|
|
137
|
+
* these declared at build time — they stamp into Info.plist `BGTaskSchedulerPermittedIdentifiers`
|
|
138
|
+
* (without them `BGTaskScheduler.register` throws) plus `fetch`+`processing` into UIBackgroundModes.
|
|
139
|
+
* Android self-initializes WorkManager via its androidx startup provider — nothing to stamp. The
|
|
140
|
+
* same ids are what `kit.backgroundTask.register(id, …)` / `.schedule({id})` use. No-op when absent
|
|
141
|
+
* or the module is inactive. */
|
|
142
|
+
backgroundTasks?: string[];
|
|
123
143
|
/** Remote push (APNs/FCM). Off unless set — gating matters: an `aps-environment` entitlement on a
|
|
124
144
|
* team that can't hold the Push capability (e.g. a personal team) BREAKS code signing, and the
|
|
125
145
|
* handshake should honestly report `push: 'none'` on an un-provisioned build. The kit returns a raw
|