@livx.cc/appwrap 0.61.13 → 0.61.14

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.13",
3
+ "version": "0.61.14",
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,4 +1,4 @@
1
- import { Application, AndroidApplication, Button, EventData, GridLayout, Image, LoadEventData, Page, StackLayout, WebView, isAndroid, isIOS, knownFolders, path } from '@nativescript/core';
1
+ import { Application, AndroidApplication, Button, EventData, 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';
@@ -180,32 +180,25 @@ export function onPageLoaded(args: EventData): void {
180
180
  * NOTE: web-process termination has no NS seam in a prod build (the didTerminate forwarder lives on
181
181
  * the debug-only DevCertNavDelegate) — out of scope here.
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
183
  /**
189
184
  * `splashHold`: keep the launch splash up until the PAGE has painted. Without it the OS splash drops
190
185
  * the moment this native page draws, and a loader:'server' WebView shows its blank canvas for the
191
186
  * whole index.html + bundle download. The page ends the hold with `kit.invoke('ui.splash.hide')`;
192
187
  * a safety timeout and the load-failure view end it too, so a page that never calls it (or an old
193
188
  * web build behind a new shell) costs at most `timeoutMs`. Returns the idempotent hide.
189
+ *
190
+ * PIXEL-IDENTICAL BY CONSTRUCTION: the overlay IS the OS launch splash, re-instantiated natively and
191
+ * pinned full-bleed over the whole window — iOS instantiates LaunchScreen.storyboard itself, Android
192
+ * paints the window-background drawable (splash_screen.xml). An NS-layout copy sat inside the safe
193
+ * area, so its logo landed lower than the storyboard's and visibly jumped at the hand-off.
194
194
  */
195
195
  function wireSplashHold(page: Page): (why: string) => void {
196
196
  const noop = () => {};
197
- const overlay = SHELL_CONFIG.splash?.hold ? page.getViewById<GridLayout>('splashOverlay') : null;
198
- if (!overlay) {
197
+ const remove = SHELL_CONFIG.splash?.hold ? mountNativeSplash(page) : null;
198
+ if (!remove) {
199
199
  bridge.register('ui.splash.hide', noop); // a page may always call it; nothing is held here
200
200
  return noop;
201
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
202
  const shownAt = Date.now();
210
203
  let done = false;
211
204
  const hide = (why: string): void => {
@@ -213,15 +206,43 @@ function wireSplashHold(page: Page): (why: string) => void {
213
206
  done = true;
214
207
  clearTimeout(timer);
215
208
  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'; });
209
+ remove();
219
210
  };
220
211
  const timer = setTimeout(() => hide('timeout'), SHELL_CONFIG.splash.timeoutMs);
221
212
  bridge.register('ui.splash.hide', () => { hide('page'); });
222
213
  return hide;
223
214
  }
224
215
 
216
+ /** Put the OS launch splash back on screen as a native full-window view; returns its fade-out+remove. */
217
+ function mountNativeSplash(page: Page): (() => void) | null {
218
+ const FADE_MS = 180;
219
+ try {
220
+ if (isIOS) {
221
+ const host = (page.viewController as UIViewController | undefined)?.view;
222
+ const vc = UIStoryboard.storyboardWithNameBundle('LaunchScreen', null).instantiateInitialViewController();
223
+ if (!host || !vc) return null;
224
+ const v = vc.view;
225
+ v.frame = UIScreen.mainScreen.bounds;
226
+ v.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
227
+ host.addSubview(v);
228
+ return () => UIView.animateWithDurationAnimationsCompletion(FADE_MS / 1000, () => { v.alpha = 0; }, () => v.removeFromSuperview());
229
+ }
230
+ const act = (Application.android.foregroundActivity || Application.android.startActivity) as android.app.Activity | null;
231
+ const decor = act?.getWindow()?.getDecorView() as android.view.ViewGroup | undefined;
232
+ if (!act || !decor) return null;
233
+ const v = new android.view.View(act);
234
+ const resId = act.getResources().getIdentifier('splash_screen', 'drawable', act.getPackageName());
235
+ if (resId) v.setBackground(act.getResources().getDrawable(resId, act.getTheme()));
236
+ else v.setBackgroundColor(android.graphics.Color.parseColor(SHELL_CONFIG.backgroundColor));
237
+ v.setClickable(true); // swallow touches while the page boots, like the OS splash
238
+ decor.addView(v, new android.view.ViewGroup.LayoutParams(-1, -1));
239
+ return () => v.animate().alpha(0).setDuration(FADE_MS).withEndAction(new java.lang.Runnable({ run: () => decor.removeView(v) })).start();
240
+ } catch (e) {
241
+ appwrapNativeLog(`[native:splash] overlay unavailable: ${e}`);
242
+ return null;
243
+ }
244
+ }
245
+
225
246
  function wireLoadFallback(page: Page, webView: CustomWebView, hideSplash: (why: string) => void = () => {}): void {
226
247
  if (SHELL_CONFIG.loader !== 'server') return; // bundled loaders can't fail on network
227
248
  const fallback = page.getViewById<StackLayout>('loadFallback');
@@ -230,6 +251,7 @@ function wireLoadFallback(page: Page, webView: CustomWebView, hideSplash: (why:
230
251
 
231
252
  let retryTimer: ReturnType<typeof setTimeout> | null = null;
232
253
  let backoffMs = 3000;
254
+ let triedCache = false; // iOS: one cache-only replay per failure run (see below)
233
255
  const clearTimer = () => { if (retryTimer) { clearTimeout(retryTimer); retryTimer = null; } };
234
256
  const retry = () => {
235
257
  clearTimer();
@@ -245,6 +267,7 @@ function wireLoadFallback(page: Page, webView: CustomWebView, hideSplash: (why:
245
267
  const err = String(args.error ?? '');
246
268
  if (!err) {
247
269
  // successful load → hide + reset (idempotent; covers the auto-retry that finally lands)
270
+ triedCache = false;
248
271
  clearTimer();
249
272
  backoffMs = 3000;
250
273
  if (fallback.visibility !== 'collapse') {
@@ -254,6 +277,20 @@ function wireLoadFallback(page: Page, webView: CustomWebView, hideSplash: (why:
254
277
  return;
255
278
  }
256
279
  if (/cancel/i.test(err)) return; // NSURLErrorCancelled: navigation superseded, not a failure
280
+ // iOS OFFLINE LAUNCH: WKWebView gets no service worker without app-bound domains, but its HTTP
281
+ // cache already holds the last index.html (no-cache = revalidate, still stored) and the immutable
282
+ // hashed assets. Before showing the error, replay the launch URL cache-only; online launches never
283
+ // take this path, so they still revalidate and get the newest deploy (OTA).
284
+ if (isIOS && !triedCache) {
285
+ triedCache = true;
286
+ const wk = webView.ios as WKWebView | null;
287
+ if (wk) {
288
+ appwrapNativeLog(`[native:fallback] load failed (${err}) — replaying from the HTTP cache`);
289
+ wk.loadRequest(NSURLRequest.requestWithURLCachePolicyTimeoutInterval(
290
+ NSURL.URLWithString(effectiveServerUrl()), NSURLRequestCachePolicy.ReturnCacheDataDontLoad, 10));
291
+ return;
292
+ }
293
+ }
257
294
  appwrapNativeLog(`[native:fallback] load failed: ${err}`);
258
295
  hideSplash('load-failed');
259
296
  fallback.visibility = 'visible';
@@ -6,11 +6,6 @@
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>
14
9
  <!-- loader:'server' load-failure fallback (App Review 2.1a): shown by main-page.ts when the
15
10
  server navigation fails (offline / server down) so the reviewer never sees a white screen. -->
16
11
  <StackLayout id="loadFallback" row="0" visibility="collapse" backgroundColor="#ffffff" verticalAlignment="center" padding="32">