@livx.cc/appwrap 0.61.11 → 0.61.13
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/iOS/Podfile +5 -2
- package/runtime/app/main-page.ts +47 -3
- package/runtime/app/main-page.xml +5 -0
- package/runtime/app/shell/config.ts +4 -0
- package/src/cli.ts +56 -13
- package/src/config.ts +12 -4
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# Silence CocoaPods' "no platform specified" warning; matches the post_install target below.
|
|
2
|
-
platform :ios, '
|
|
2
|
+
platform :ios, '15.0'
|
|
3
3
|
|
|
4
4
|
# Bump every pod's deployment target. Xcode 15+ removed `libarclite`, so pods that
|
|
5
5
|
# declare an old IPHONEOS_DEPLOYMENT_TARGET (e.g. SAMKeychain via @nativescript/
|
|
6
6
|
# secure-storage) fail device builds with "SDK does not contain 'libarclite'".
|
|
7
|
+
# 15.0 is the FLOOR, not a preference: Xcode 27 rejects any IPHONEOS_DEPLOYMENT_TARGET under 15.0
|
|
8
|
+
# as a hard error ("supported deployment target versions is 15.0 to 27.0.x"), so 13.0 failed every
|
|
9
|
+
# device build there while older Xcodes still accepted it. An app's own target may sit higher.
|
|
7
10
|
post_install do |installer|
|
|
8
11
|
installer.pods_project.targets.each do |target|
|
|
9
12
|
target.build_configurations.each do |config|
|
|
10
|
-
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '
|
|
13
|
+
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
|
|
11
14
|
end
|
|
12
15
|
end
|
|
13
16
|
end
|
package/runtime/app/main-page.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Application, AndroidApplication, Button, EventData, LoadEventData, Page, StackLayout, WebView, isAndroid, isIOS, knownFolders, path } from '@nativescript/core';
|
|
1
|
+
import { Application, AndroidApplication, Button, EventData, GridLayout, Image, LoadEventData, Page, StackLayout, WebView, isAndroid, isIOS, knownFolders, path } from '@nativescript/core';
|
|
2
2
|
import { bridge } from './shell/bridge';
|
|
3
3
|
import { effectiveServerUrl } from './shell/server-url';
|
|
4
4
|
import { registerHandlers } from './shell/handlers';
|
|
@@ -94,6 +94,7 @@ export function onPageLoaded(args: EventData): void {
|
|
|
94
94
|
|
|
95
95
|
if (initialized) return;
|
|
96
96
|
initialized = true;
|
|
97
|
+
const hideSplash = wireSplashHold(page); // first: the overlay must be up before the first frame
|
|
97
98
|
|
|
98
99
|
registerHandlers();
|
|
99
100
|
registerExtendedHandlers();
|
|
@@ -126,7 +127,7 @@ export function onPageLoaded(args: EventData): void {
|
|
|
126
127
|
// if a future surface orphans a window with no dismiss callback — gate it on a "surface presented" flag.
|
|
127
128
|
void armNativeSurfaceRecovery; // referenced to keep the helper (still callable if a scoped need arises)
|
|
128
129
|
if (isAndroid) wireAndroidSafeArea(webView); // experimental edge-to-edge (no-op unless config on)
|
|
129
|
-
wireLoadFallback(page, webView); // loader:'server' failure → branded retry view (no white screen)
|
|
130
|
+
wireLoadFallback(page, webView, hideSplash); // loader:'server' failure → branded retry view (no white screen)
|
|
130
131
|
startEventForwarding();
|
|
131
132
|
loadBundle(webView);
|
|
132
133
|
// Env indicator banner: shown in the bottom safe area on relaunch when a non-default env override is
|
|
@@ -179,7 +180,49 @@ export function onPageLoaded(args: EventData): void {
|
|
|
179
180
|
* NOTE: web-process termination has no NS seam in a prod build (the didTerminate forwarder lives on
|
|
180
181
|
* the debug-only DevCertNavDelegate) — out of scope here.
|
|
181
182
|
*/
|
|
182
|
-
|
|
183
|
+
/** iOS LaunchScreen.Center is stamped at 220pt; Android's splash canvas is 288dp (cli.ts
|
|
184
|
+
* ANDROID_SPLASH_DP). The overlay draws the SAME image at the SAME size so the hand-off from the OS
|
|
185
|
+
* launch screen is invisible. */
|
|
186
|
+
const SPLASH_LOGO_SIZE = isIOS ? 220 : 288;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* `splashHold`: keep the launch splash up until the PAGE has painted. Without it the OS splash drops
|
|
190
|
+
* the moment this native page draws, and a loader:'server' WebView shows its blank canvas for the
|
|
191
|
+
* whole index.html + bundle download. The page ends the hold with `kit.invoke('ui.splash.hide')`;
|
|
192
|
+
* a safety timeout and the load-failure view end it too, so a page that never calls it (or an old
|
|
193
|
+
* web build behind a new shell) costs at most `timeoutMs`. Returns the idempotent hide.
|
|
194
|
+
*/
|
|
195
|
+
function wireSplashHold(page: Page): (why: string) => void {
|
|
196
|
+
const noop = () => {};
|
|
197
|
+
const overlay = SHELL_CONFIG.splash?.hold ? page.getViewById<GridLayout>('splashOverlay') : null;
|
|
198
|
+
if (!overlay) {
|
|
199
|
+
bridge.register('ui.splash.hide', noop); // a page may always call it; nothing is held here
|
|
200
|
+
return noop;
|
|
201
|
+
}
|
|
202
|
+
const logo = page.getViewById<Image>('splashLogo');
|
|
203
|
+
if (SHELL_CONFIG.splash.logo && logo) {
|
|
204
|
+
logo.src = isIOS ? 'res://LaunchScreen.Center' : 'res://splash_logo';
|
|
205
|
+
logo.width = SPLASH_LOGO_SIZE;
|
|
206
|
+
logo.height = SPLASH_LOGO_SIZE;
|
|
207
|
+
}
|
|
208
|
+
overlay.visibility = 'visible';
|
|
209
|
+
const shownAt = Date.now();
|
|
210
|
+
let done = false;
|
|
211
|
+
const hide = (why: string): void => {
|
|
212
|
+
if (done) return;
|
|
213
|
+
done = true;
|
|
214
|
+
clearTimeout(timer);
|
|
215
|
+
appwrapNativeLog(`[native:splash] hide (${why}) after ${Date.now() - shownAt}ms`);
|
|
216
|
+
overlay.animate({ opacity: 0, duration: 180 })
|
|
217
|
+
.catch(() => { /* view torn down mid-fade — collapsing below is all that matters */ })
|
|
218
|
+
.then(() => { overlay.visibility = 'collapse'; });
|
|
219
|
+
};
|
|
220
|
+
const timer = setTimeout(() => hide('timeout'), SHELL_CONFIG.splash.timeoutMs);
|
|
221
|
+
bridge.register('ui.splash.hide', () => { hide('page'); });
|
|
222
|
+
return hide;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function wireLoadFallback(page: Page, webView: CustomWebView, hideSplash: (why: string) => void = () => {}): void {
|
|
183
226
|
if (SHELL_CONFIG.loader !== 'server') return; // bundled loaders can't fail on network
|
|
184
227
|
const fallback = page.getViewById<StackLayout>('loadFallback');
|
|
185
228
|
const retryBtn = page.getViewById<Button>('loadRetryBtn');
|
|
@@ -212,6 +255,7 @@ function wireLoadFallback(page: Page, webView: CustomWebView): void {
|
|
|
212
255
|
}
|
|
213
256
|
if (/cancel/i.test(err)) return; // NSURLErrorCancelled: navigation superseded, not a failure
|
|
214
257
|
appwrapNativeLog(`[native:fallback] load failed: ${err}`);
|
|
258
|
+
hideSplash('load-failed');
|
|
215
259
|
fallback.visibility = 'visible';
|
|
216
260
|
if (!retryTimer) {
|
|
217
261
|
retryTimer = setTimeout(() => { backoffMs = Math.min(backoffMs * 2, 30_000); retry(); }, backoffMs);
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
backgroundColor="{{ backgroundColor }}">
|
|
7
7
|
<GridLayout rows="*">
|
|
8
8
|
<shell:CustomWebView id="webview" row="0" />
|
|
9
|
+
<!-- splashHold: the launch splash (bg + splashIcon) held over the WebView until the page reports
|
|
10
|
+
its first paint (`ui.splash.hide`) — see wireSplashHold in main-page.ts. -->
|
|
11
|
+
<GridLayout id="splashOverlay" row="0" visibility="collapse" backgroundColor="{{ backgroundColor }}" iosOverflowSafeArea="true">
|
|
12
|
+
<Image id="splashLogo" stretch="aspectFit" horizontalAlignment="center" verticalAlignment="center" />
|
|
13
|
+
</GridLayout>
|
|
9
14
|
<!-- loader:'server' load-failure fallback (App Review 2.1a): shown by main-page.ts when the
|
|
10
15
|
server navigation fails (offline / server down) so the reviewer never sees a white screen. -->
|
|
11
16
|
<StackLayout id="loadFallback" row="0" visibility="collapse" backgroundColor="#ffffff" verticalAlignment="center" padding="32">
|
|
@@ -73,6 +73,10 @@ export const SHELL_CONFIG = {
|
|
|
73
73
|
* Lifting extra makes the page cover that strip (the keyboard hides the thin bottom row of content).
|
|
74
74
|
* Default 82. Set 0 for NO extra lift (input sits flush at the reported height; the strip may show). */
|
|
75
75
|
iosKeyboardExtraLift: 82,
|
|
76
|
+
/** `splashHold` config: hold the launch splash over the WebView until the page calls
|
|
77
|
+
* `ui.splash.hide` (or `timeoutMs` passes / the load fails). `logo` = a `splashIcon` was stamped
|
|
78
|
+
* (iOS `LaunchScreen.Center` imageset, Android `splash_logo` drawable). See wireSplashHold. */
|
|
79
|
+
splash: { hold: false, timeoutMs: 4000, logo: false } as { hold: boolean; timeoutMs: number; logo: boolean },
|
|
76
80
|
/** Runtime env-switcher (loader:'server'). `enabled` is the resolved kill-switch (config block
|
|
77
81
|
* present AND not `enabled:false`); when false the menu action, banner, and boot override are all
|
|
78
82
|
* inert. `envs` = declared presets; `allowPattern` = anchored regex gating "Other" (default-deny
|
package/src/cli.ts
CHANGED
|
@@ -974,6 +974,12 @@ export function stampShellConfig(outDir: string, cfg: AppwrapConfig): void {
|
|
|
974
974
|
envs: (es?.envs ?? []).map((e) => ({ label: String(e.label), url: String(e.url) })),
|
|
975
975
|
allowPattern: es?.allowPattern ?? '',
|
|
976
976
|
};
|
|
977
|
+
const hold = cfg.splashHold;
|
|
978
|
+
const splash = {
|
|
979
|
+
hold: !!hold,
|
|
980
|
+
timeoutMs: (typeof hold === 'object' && hold.timeoutMs) || 4000,
|
|
981
|
+
logo: !!cfg.splashIcon,
|
|
982
|
+
};
|
|
977
983
|
const content = `/**
|
|
978
984
|
* Shell config — stamped by \`appwrap init\`/\`sync\` from the appwrap config. Do not edit.
|
|
979
985
|
*/
|
|
@@ -1002,6 +1008,7 @@ export const SHELL_CONFIG = {
|
|
|
1002
1008
|
pushAndroid: ${JSON.stringify(!!cfg.push?.enabled && cfg.push?.android !== false)},
|
|
1003
1009
|
pushRegistrationUrl: ${JSON.stringify(cfg.push?.registrationUrl ?? '')},
|
|
1004
1010
|
iosKeyboardExtraLift: ${JSON.stringify(cfg.iosKeyboardExtraLift ?? 82)},
|
|
1011
|
+
splash: ${JSON.stringify(splash)} as { hold: boolean; timeoutMs: number; logo: boolean },
|
|
1005
1012
|
envSwitcher: ${JSON.stringify(envSwitcher)} as { enabled: boolean; envs: { label: string; url: string }[]; allowPattern: string },
|
|
1006
1013
|
webCaps: ${JSON.stringify(webCaps)} as { camera: boolean; microphone: boolean; geolocation: boolean },
|
|
1007
1014
|
};
|
|
@@ -1895,6 +1902,14 @@ function generateIcons(cwd: string, outDir: string, cfg: AppwrapConfig): void {
|
|
|
1895
1902
|
ras.resize(splashSrc, Math.round(220 * parseFloat(img.scale)), join(centerSet, img.filename));
|
|
1896
1903
|
}
|
|
1897
1904
|
}
|
|
1905
|
+
// Android: the same logo on the 288dp splash canvas (xxxhdpi-sharp; nodpi so the layer-list's
|
|
1906
|
+
// explicit dp size does the scaling), then re-stamp the splash XML + Android 12+ theme to use it.
|
|
1907
|
+
const droidRes = join(outDir, 'App_Resources/Android/src/main/res');
|
|
1908
|
+
const hex = (cfg.backgroundColor ?? '#ffffff').replace('#', '');
|
|
1909
|
+
if (existsSync(splashSrc) && existsSync(droidRes) && /^[0-9a-fA-F]{6}$/.test(hex)) {
|
|
1910
|
+
ras.resize(splashSrc, ANDROID_SPLASH_DP * 4, join(droidRes, 'drawable-nodpi/splash_logo.png'));
|
|
1911
|
+
writeAndroidSplash(outDir, hex, true);
|
|
1912
|
+
}
|
|
1898
1913
|
}
|
|
1899
1914
|
|
|
1900
1915
|
const ANDROID_DENSITIES: Record<string, number> = { mdpi: 48, hdpi: 72, xhdpi: 96, xxhdpi: 144, xxxhdpi: 192 };
|
|
@@ -1967,19 +1982,47 @@ function stampLaunchScreen(outDir: string, cfg: AppwrapConfig): void {
|
|
|
1967
1982
|
|
|
1968
1983
|
// Android: replace the default NativeScript splash (background bitmap + NS logo) with a solid fill
|
|
1969
1984
|
// of the app's backgroundColor — parity with the iOS solid launch screen, and no NS branding flash.
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
}
|
|
1985
|
+
writeAndroidSplash(outDir, hex, false);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
/** Android launch splash, both phases, on one `ANDROID_SPLASH_DP` canvas so the logo never jumps:
|
|
1989
|
+
* the activity window background (`splash_screen.xml`, every API level) and — with a logo — the
|
|
1990
|
+
* Android 12+ system SplashScreen (`values-v31` LaunchScreenTheme), whose icon is drawn at 288dp when
|
|
1991
|
+
* it has no icon background. The shell's splash-hold overlay (main-page.ts) uses the same size. */
|
|
1992
|
+
const ANDROID_SPLASH_DP = 288;
|
|
1993
|
+
function writeAndroidSplash(outDir: string, hex: string, logo: boolean): void {
|
|
1994
|
+
const res = join(outDir, 'App_Resources/Android/src/main/res');
|
|
1995
|
+
const splash = join(res, 'drawable-nodpi/splash_screen.xml');
|
|
1996
|
+
if (!existsSync(splash)) return;
|
|
1997
|
+
const color = `#${hex.toUpperCase()}`;
|
|
1998
|
+
writeFileSync(splash,
|
|
1999
|
+
`<?xml version="1.0" encoding="utf-8"?>\n` +
|
|
2000
|
+
`<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:gravity="fill">\n` +
|
|
2001
|
+
` <item>\n` +
|
|
2002
|
+
` <shape android:shape="rectangle">\n` +
|
|
2003
|
+
` <solid android:color="${color}" />\n` +
|
|
2004
|
+
` </shape>\n` +
|
|
2005
|
+
` </item>\n` +
|
|
2006
|
+
(logo
|
|
2007
|
+
? ` <item android:width="${ANDROID_SPLASH_DP}dp" android:height="${ANDROID_SPLASH_DP}dp" android:gravity="center">\n` +
|
|
2008
|
+
` <bitmap android:src="@drawable/splash_logo" android:gravity="fill" />\n` +
|
|
2009
|
+
` </item>\n`
|
|
2010
|
+
: '') +
|
|
2011
|
+
`</layer-list>\n`
|
|
2012
|
+
);
|
|
2013
|
+
const v31 = join(res, 'values-v31/styles.xml');
|
|
2014
|
+
if (!logo) { if (existsSync(v31) && readFileSync(v31, 'utf8').includes('appwrap:splash')) rmSync(v31); return; }
|
|
2015
|
+
mkdirSync(dirname(v31), { recursive: true });
|
|
2016
|
+
writeFileSync(v31,
|
|
2017
|
+
`<?xml version="1.0" encoding="utf-8"?>\n` +
|
|
2018
|
+
`<!-- appwrap:splash — generated from splashIcon/backgroundColor; regenerated every sync. -->\n` +
|
|
2019
|
+
`<resources xmlns:android="http://schemas.android.com/apk/res/android">\n` +
|
|
2020
|
+
` <style name="LaunchScreenTheme" parent="LaunchScreenThemeBase">\n` +
|
|
2021
|
+
` <item name="android:windowSplashScreenBackground">${color}</item>\n` +
|
|
2022
|
+
` <item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_logo</item>\n` +
|
|
2023
|
+
` </style>\n` +
|
|
2024
|
+
`</resources>\n`
|
|
2025
|
+
);
|
|
1983
2026
|
}
|
|
1984
2027
|
|
|
1985
2028
|
const VERSION_FILE = '.appwrap-version';
|
package/src/config.ts
CHANGED
|
@@ -169,10 +169,18 @@ export interface AppwrapConfig {
|
|
|
169
169
|
oauthRedirectSchemes?: string[];
|
|
170
170
|
/** App icon source (≥512px square png). Defaults to the largest icon in the PWA manifest. */
|
|
171
171
|
icon?: string;
|
|
172
|
-
/** Optional centered logo for the
|
|
173
|
-
*
|
|
174
|
-
*
|
|
172
|
+
/** Optional centered logo for the launch splash (iOS LaunchScreen + Android splash, incl. the
|
|
173
|
+
* Android 12+ system splash) — a TRANSPARENT-background square png (a wordmark or glyph, NOT the
|
|
174
|
+
* app icon, whose opaque background would show as a box). Rendered on a 220pt (iOS) / 288dp
|
|
175
|
+
* (Android) canvas, so bake the glyph's padding into the png (~45% of the canvas keeps a square
|
|
176
|
+
* inside Android 12's 192dp circle mask). When absent the splash is a solid `backgroundColor`. */
|
|
175
177
|
splashIcon?: string;
|
|
178
|
+
/** Hold the launch splash (backgroundColor + `splashIcon`) OVER the WebView until the page says it
|
|
179
|
+
* has painted — `kit.invoke('ui.splash.hide')` — instead of dropping it the moment the native page
|
|
180
|
+
* draws (which shows the WebView's blank canvas while a loader:'server' page downloads). Safety
|
|
181
|
+
* net: it hides anyway after `timeoutMs` (default 4000) or when the load-failure view shows.
|
|
182
|
+
* `true` = defaults. Off by default: a page that never calls hide would sit behind the splash. */
|
|
183
|
+
splashHold?: boolean | { timeoutMs?: number };
|
|
176
184
|
/** Loader: 'app' (default — app:// scheme, ES modules OK), 'file' (debug fallback), or 'server'
|
|
177
185
|
* (load `serverUrl` live — dev HMR over LAN or a deployed URL). `appwrap dev` sets this. */
|
|
178
186
|
loader?: 'app' | 'file' | 'server';
|
|
@@ -401,7 +409,7 @@ export const KNOWN_CONFIG_KEYS: ReadonlySet<string> = new Set([
|
|
|
401
409
|
'debugLog', 'desktop', 'devMenu', 'edgeToEdge', 'entry', 'icon', 'id', 'iosKeyboardExtraLift', 'loader', 'modules', 'modulePacks', 'name',
|
|
402
410
|
'androidLockTextZoom', 'envSwitcher', 'iosEntitlements', 'iosInfoPlist', 'neutralizeServiceWorker', 'oauthRedirectSchemes', 'openNewWindowsInBrowser', 'orientation', 'overrides', 'permissions',
|
|
403
411
|
'plugins', 'push', 'pwaDist', 'queryPackages', 'queryUrlSchemes', 'serverUrl', 'shareTarget', 'signing', 'signingProfiles', 'statusBarStyle', 'store',
|
|
404
|
-
'splashIcon', 'storekitConfig', 'targetedDevices', 'teamId', 'themeColor', 'trackingDomains', 'urlScheme',
|
|
412
|
+
'splashHold', 'splashIcon', 'storekitConfig', 'targetedDevices', 'teamId', 'themeColor', 'trackingDomains', 'urlScheme',
|
|
405
413
|
'usesNonExemptEncryption', 'vendorPaths', 'version',
|
|
406
414
|
]);
|
|
407
415
|
|