@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livx.cc/appwrap",
3
- "version": "0.61.11",
3
+ "version": "0.61.13",
4
4
  "description": "Wrap any PWA into a native app with native capabilities (appwrap runtime + @livx.cc/native-kit).",
5
5
  "license": "MIT",
6
6
  "author": "Elya Livshitz",
@@ -1,13 +1,16 @@
1
1
  # Silence CocoaPods' "no platform specified" warning; matches the post_install target below.
2
- platform :ios, '13.0'
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.0'
13
+ config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
11
14
  end
12
15
  end
13
16
  end
@@ -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
- function wireLoadFallback(page: Page, webView: CustomWebView): void {
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
- const splash = join(outDir, 'App_Resources/Android/src/main/res/drawable-nodpi/splash_screen.xml');
1971
- if (existsSync(splash)) {
1972
- writeFileSync(splash,
1973
- `<?xml version="1.0" encoding="utf-8"?>\n` +
1974
- `<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:gravity="fill">\n` +
1975
- ` <item>\n` +
1976
- ` <shape android:shape="rectangle">\n` +
1977
- ` <solid android:color="#${hex.toUpperCase()}" />\n` +
1978
- ` </shape>\n` +
1979
- ` </item>\n` +
1980
- `</layer-list>\n`
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 iOS launch splash — a TRANSPARENT-background png (a wordmark or
173
- * glyph, NOT the app icon, whose opaque background would show as a box on the splash). When absent
174
- * the splash is a clean solid `backgroundColor` fill with no logo. */
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