@livx.cc/appwrap 0.20.4 → 0.21.0
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/main-page.ts +3 -0
- package/runtime/app/shell/banner.ts +149 -0
- package/runtime/app/shell/bridge.ts +7 -1
- package/runtime/app/shell/capabilities.manifest.ts +1 -1
- package/runtime/app/shell/config.ts +2 -0
- package/runtime/app/shell/devmenu.ts +144 -0
- package/runtime/app/shell/handlers.ts +28 -2
- package/src/cli.ts +1 -0
- package/src/config.ts +4 -0
- package/templates/ci/github/workflows/appwrap-pr.yml +2 -2
- package/templates/ci/github/workflows/appwrap-release-android.yml +1 -1
- package/templates/ci/github/workflows/appwrap-release-ios.yml +1 -1
package/package.json
CHANGED
package/runtime/app/main-page.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { registerAndroidHandlers } from './shell/handlers-android';
|
|
|
11
11
|
import { registerOptionalHandlers } from './shell/optional-handlers.generated';
|
|
12
12
|
import './shell/fcm-bootstrap.generated'; // side-effect: registers the FCM service when push is wired
|
|
13
13
|
import { startEventForwarding } from './shell/events';
|
|
14
|
+
import { startDevMenu } from './shell/devmenu';
|
|
14
15
|
import { SHELL_CONFIG } from './shell/config';
|
|
15
16
|
import { bindStatusBarPage, setStatusBarStyle, enableAndroidEdgeToEdge, wireAndroidSafeArea } from './shell/status-bar';
|
|
16
17
|
import { CustomWebView } from './shell/custom-webview';
|
|
@@ -44,6 +45,8 @@ export function onPageLoaded(args: EventData): void {
|
|
|
44
45
|
if (isAndroid) registerAndroidHandlers();
|
|
45
46
|
// Opt-in modules that own their own handler file (health, …) — generated to only the active set.
|
|
46
47
|
registerOptionalHandlers();
|
|
48
|
+
// Shake-to-open developer menu (App Info / Reload). On by default, incl. prod.
|
|
49
|
+
if (SHELL_CONFIG.devMenu) startDevMenu();
|
|
47
50
|
|
|
48
51
|
const webView = page.getViewById<CustomWebView>('webview');
|
|
49
52
|
bridge.attach(webView);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Application, Utils, isAndroid, isIOS } from '@nativescript/core';
|
|
2
|
+
import { bridge } from './bridge';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Persistent bottom banner (vs. the auto-dismiss `toast`). Used for the "new version available —
|
|
6
|
+
* tap to reload" update prompt. **Tap** emits `toast.action` { id } (the web side decides what to
|
|
7
|
+
* do) and dismisses; **swipe down** dismisses without acting.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
let currentId: string | null = null;
|
|
11
|
+
let iosBanner: UIView | null = null;
|
|
12
|
+
let iosTapHandler: any = null;
|
|
13
|
+
let androidBanner: android.view.View | null = null;
|
|
14
|
+
|
|
15
|
+
function onTap(): void {
|
|
16
|
+
const id = currentId;
|
|
17
|
+
dismissBanner();
|
|
18
|
+
if (id) bridge.emit('toast.action', { id });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function showBanner(opts: { id: string; message: string }): void {
|
|
22
|
+
dismissBanner(); // only one at a time
|
|
23
|
+
currentId = opts.id;
|
|
24
|
+
if (isIOS) Utils.dispatchToMainThread(() => showIOSBanner(opts.message));
|
|
25
|
+
else if (isAndroid) runOnAndroidUi(() => showAndroidBanner(opts.message));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function dismissBanner(): void {
|
|
29
|
+
currentId = null;
|
|
30
|
+
if (isIOS && iosBanner) {
|
|
31
|
+
const b = iosBanner;
|
|
32
|
+
iosBanner = null;
|
|
33
|
+
iosTapHandler = null;
|
|
34
|
+
Utils.dispatchToMainThread(() => b.removeFromSuperview());
|
|
35
|
+
}
|
|
36
|
+
if (isAndroid && androidBanner) {
|
|
37
|
+
const v = androidBanner;
|
|
38
|
+
androidBanner = null;
|
|
39
|
+
runOnAndroidUi(() => (v.getParent() as android.view.ViewGroup)?.removeView(v));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── iOS ──────────────────────────────────────────────────────────────
|
|
44
|
+
// Built lazily (and only on iOS) — `NSObject`/`interop` don't exist on Android, and this file is
|
|
45
|
+
// imported on both platforms via handlers.ts, so a top-level `.extend` would crash the Android shell.
|
|
46
|
+
let IOSGestureHandler: any = null;
|
|
47
|
+
function iosGestureHandlerClass(): any {
|
|
48
|
+
if (!IOSGestureHandler) {
|
|
49
|
+
IOSGestureHandler = (NSObject as any).extend(
|
|
50
|
+
{ bannerTapped() { onTap(); }, bannerDismissed() { dismissBanner(); } },
|
|
51
|
+
{
|
|
52
|
+
exposedMethods: {
|
|
53
|
+
bannerTapped: { returns: interop.types.void },
|
|
54
|
+
bannerDismissed: { returns: interop.types.void },
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return IOSGestureHandler;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function showIOSBanner(message: string): void {
|
|
63
|
+
const rootVC = Utils.ios.getRootViewController();
|
|
64
|
+
if (!rootVC?.view) return;
|
|
65
|
+
const screen = UIScreen.mainScreen.bounds;
|
|
66
|
+
const safeBottom = rootVC.view.safeAreaInsets ? rootVC.view.safeAreaInsets.bottom : 0;
|
|
67
|
+
const width = screen.size.width - 24;
|
|
68
|
+
|
|
69
|
+
const container = UIView.alloc().initWithFrame(CGRectMake(12, 0, width, 52));
|
|
70
|
+
container.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0.08, 0.08, 0.1, 0.85);
|
|
71
|
+
container.layer.cornerRadius = 12;
|
|
72
|
+
container.clipsToBounds = true;
|
|
73
|
+
container.userInteractionEnabled = true;
|
|
74
|
+
|
|
75
|
+
const blur = UIVisualEffectView.alloc().initWithEffect(UIBlurEffect.effectWithStyle(UIBlurEffectStyle.Dark));
|
|
76
|
+
blur.frame = container.bounds;
|
|
77
|
+
blur.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
|
|
78
|
+
blur.userInteractionEnabled = false;
|
|
79
|
+
container.addSubview(blur);
|
|
80
|
+
|
|
81
|
+
const label = UILabel.alloc().initWithFrame(CGRectMake(16, 0, width - 32, 52));
|
|
82
|
+
label.text = message;
|
|
83
|
+
label.textColor = UIColor.whiteColor;
|
|
84
|
+
label.textAlignment = NSTextAlignment.Center;
|
|
85
|
+
label.font = UIFont.systemFontOfSizeWeight(15, UIFontWeightSemibold);
|
|
86
|
+
blur.contentView.addSubview(label);
|
|
87
|
+
|
|
88
|
+
iosTapHandler = iosGestureHandlerClass().alloc().init();
|
|
89
|
+
container.addGestureRecognizer(UITapGestureRecognizer.alloc().initWithTargetAction(iosTapHandler, 'bannerTapped'));
|
|
90
|
+
const swipe = UISwipeGestureRecognizer.alloc().initWithTargetAction(iosTapHandler, 'bannerDismissed');
|
|
91
|
+
swipe.direction = UISwipeGestureRecognizerDirection.Down;
|
|
92
|
+
container.addGestureRecognizer(swipe);
|
|
93
|
+
|
|
94
|
+
container.center = CGPointMake(screen.size.width / 2, screen.size.height - safeBottom - 38);
|
|
95
|
+
container.alpha = 0;
|
|
96
|
+
rootVC.view.addSubview(container);
|
|
97
|
+
iosBanner = container;
|
|
98
|
+
UIView.animateWithDurationAnimations(0.3, () => (container.alpha = 1));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Android ──────────────────────────────────────────────────────────
|
|
102
|
+
function showAndroidBanner(message: string): void {
|
|
103
|
+
const activity = Application.android.foregroundActivity || Application.android.startActivity;
|
|
104
|
+
if (!activity) return;
|
|
105
|
+
const density = activity.getResources().getDisplayMetrics().density;
|
|
106
|
+
const pad = Math.round(16 * density);
|
|
107
|
+
|
|
108
|
+
const tv = new android.widget.TextView(activity);
|
|
109
|
+
tv.setText(message);
|
|
110
|
+
tv.setTextColor(android.graphics.Color.WHITE);
|
|
111
|
+
tv.setTextSize(15);
|
|
112
|
+
tv.setPadding(pad, pad, pad, pad);
|
|
113
|
+
tv.setGravity(android.view.Gravity.CENTER);
|
|
114
|
+
tv.setBackgroundColor(android.graphics.Color.argb(230, 20, 20, 26));
|
|
115
|
+
|
|
116
|
+
// Distinguish a tap (→ reload) from a downward swipe (→ dismiss) on one touch listener.
|
|
117
|
+
const SWIPE = 40 * density;
|
|
118
|
+
let downX = 0, downY = 0, downT = 0;
|
|
119
|
+
tv.setOnTouchListener(new android.view.View.OnTouchListener({
|
|
120
|
+
onTouch(_v: any, e: any) {
|
|
121
|
+
switch (e.getActionMasked()) {
|
|
122
|
+
case android.view.MotionEvent.ACTION_DOWN:
|
|
123
|
+
downX = e.getRawX(); downY = e.getRawY(); downT = e.getEventTime();
|
|
124
|
+
return true;
|
|
125
|
+
case android.view.MotionEvent.ACTION_UP: {
|
|
126
|
+
const dy = e.getRawY() - downY;
|
|
127
|
+
const dx = Math.abs(e.getRawX() - downX);
|
|
128
|
+
const dt = e.getEventTime() - downT;
|
|
129
|
+
if (dy > SWIPE) dismissBanner(); // swipe down → dismiss
|
|
130
|
+
else if (Math.abs(dy) < SWIPE && dx < SWIPE && dt < 400) onTap(); // tap → reload
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
default:
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
const lp = new android.widget.FrameLayout.LayoutParams(-1, -2); // MATCH_PARENT × WRAP_CONTENT
|
|
140
|
+
lp.gravity = android.view.Gravity.BOTTOM;
|
|
141
|
+
activity.addContentView(tv, lp);
|
|
142
|
+
androidBanner = tv;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function runOnAndroidUi(fn: () => void): void {
|
|
146
|
+
const activity = Application.android.foregroundActivity || Application.android.startActivity;
|
|
147
|
+
if (activity) activity.runOnUiThread(new java.lang.Runnable({ run: fn }));
|
|
148
|
+
else fn();
|
|
149
|
+
}
|
|
@@ -39,6 +39,11 @@ export class Bridge {
|
|
|
39
39
|
this.webView = null;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** The attached WebView (for handlers that drive it directly, e.g. app.reload). */
|
|
43
|
+
getWebView(): CustomWebView | null {
|
|
44
|
+
return this.webView;
|
|
45
|
+
}
|
|
46
|
+
|
|
42
47
|
emit(event: string, payload?: unknown): void {
|
|
43
48
|
this.deliver(JSON.stringify({ v: 1, kind: 'event', event, payload }));
|
|
44
49
|
}
|
|
@@ -75,7 +80,8 @@ export class Bridge {
|
|
|
75
80
|
this.evalJs(js).catch((e) => console.error('Bridge: deliver failed', e));
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
|
|
83
|
+
/** Evaluate JS in the WebView and resolve its value (used by deliver + the dev-menu version probe). */
|
|
84
|
+
evalJs(script: string): Promise<any> {
|
|
79
85
|
const wv = this.webView;
|
|
80
86
|
return new Promise((resolve, reject) => {
|
|
81
87
|
if (!wv) return reject(new Error('no webview'));
|
|
@@ -68,7 +68,7 @@ export const MODULES: ModuleManifest[] = [
|
|
|
68
68
|
{ name: 'haptics', core: true, group: 'core', capabilities: { haptics: 'native' } },
|
|
69
69
|
{ name: 'share', core: true, group: 'core', capabilities: { share: 'native', shareFiles: 'native' } },
|
|
70
70
|
{ name: 'storage', core: true, group: 'core', capabilities: { storage: 'native', secureStorage: 'native' } },
|
|
71
|
-
{ name: 'toast', core: true, group: 'core', capabilities: { toast: 'native' } },
|
|
71
|
+
{ name: 'toast', core: true, group: 'core', capabilities: { toast: 'native', banner: 'native', updates: 'native' } },
|
|
72
72
|
{ name: 'statusBar', core: true, group: 'core', capabilities: { statusBar: 'native', themeColor: 'native' } },
|
|
73
73
|
{ name: 'device', core: true, group: 'extended', capabilities: { device: 'native' } },
|
|
74
74
|
{ name: 'clipboard', core: true, group: 'extended', capabilities: { clipboard: 'native' } },
|
|
@@ -27,6 +27,8 @@ export const SHELL_CONFIG = {
|
|
|
27
27
|
debug: false,
|
|
28
28
|
/** Value written to `localStorage.DEBUG` in debug mode so the PWA logger goes verbose ('*' = all). */
|
|
29
29
|
debugLog: '*',
|
|
30
|
+
/** Shake-to-open developer menu (App Info / Reload). On by default, including store builds. */
|
|
31
|
+
devMenu: true,
|
|
30
32
|
/** Remote push configured, per platform (iOS aps-environment entitlement / Android FCM). Drives the
|
|
31
33
|
* `push` capability flag at runtime by platform — off unless `appwrap.json.push` enables it, so an
|
|
32
34
|
* un-provisioned build honestly reports 'none' (and a personal-team iOS build keeps `pushIos:false`). */
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { Device, Dialogs, Utils, isAndroid, isIOS } from '@nativescript/core';
|
|
2
|
+
import { bridge } from './bridge';
|
|
3
|
+
import { SHELL_CONFIG } from './config';
|
|
4
|
+
import { SHELL_BUILD, reloadWebView } from './handlers';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shake-to-open developer menu (enabled in prod too, gated by `SHELL_CONFIG.devMenu`).
|
|
8
|
+
* A shake raises a native action sheet → "App Info" shows non-sensitive diagnostics
|
|
9
|
+
* (ids, versions, loader, remote host) including the running webapp's version vs. the
|
|
10
|
+
* latest deployed — so you can tell at a glance whether the device got the update.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Last version info the web side reported (native-kit's updates module). */
|
|
14
|
+
let webInfo: { current?: string; latest?: string; build?: string | number; updateAvailable?: boolean } = {};
|
|
15
|
+
|
|
16
|
+
export function startDevMenu(): void {
|
|
17
|
+
// The web side (native-kit updates) pushes its version + the latest-deployed version here.
|
|
18
|
+
bridge.register('app.reportWebVersion', (p: any) => {
|
|
19
|
+
webInfo = p || {};
|
|
20
|
+
});
|
|
21
|
+
if (isIOS) startIOSShake();
|
|
22
|
+
else if (isAndroid) startAndroidShake();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── shake detection (total acceleration magnitude in g incl. gravity, debounced) ──
|
|
26
|
+
const SHAKE_G = 1.8; // spike vs. ~1g rest
|
|
27
|
+
let shakeMm: any = null; // hold a ref so CoreMotion isn't GC'd
|
|
28
|
+
let firstSpikeAt = 0;
|
|
29
|
+
let spikes = 0;
|
|
30
|
+
let lastMenuAt = 0;
|
|
31
|
+
|
|
32
|
+
function onAccelMagnitude(g: number): void {
|
|
33
|
+
if (g < SHAKE_G) return;
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
if (now - lastMenuAt < 1500) return; // don't re-open right after a menu
|
|
36
|
+
if (now - firstSpikeAt > 800) {
|
|
37
|
+
firstSpikeAt = now; // start a fresh window
|
|
38
|
+
spikes = 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (++spikes >= 2) {
|
|
42
|
+
spikes = 0;
|
|
43
|
+
firstSpikeAt = 0;
|
|
44
|
+
lastMenuAt = now;
|
|
45
|
+
void showDevMenu();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function startIOSShake(): void {
|
|
50
|
+
// Poll `mm.deviceMotion` on a JS timer — CoreMotion's queue-handler block is fragile under
|
|
51
|
+
// NativeScript (can silently stop firing on-device); mirrors the motion.start handler.
|
|
52
|
+
const mm = CMMotionManager.new();
|
|
53
|
+
if (!mm.deviceMotionAvailable) return; // no sensors (simulator)
|
|
54
|
+
shakeMm = mm;
|
|
55
|
+
mm.deviceMotionUpdateInterval = 0.1;
|
|
56
|
+
mm.startDeviceMotionUpdates();
|
|
57
|
+
setInterval(() => {
|
|
58
|
+
const m = mm.deviceMotion;
|
|
59
|
+
if (!m) return; // first sample not ready
|
|
60
|
+
const x = m.userAcceleration.x + m.gravity.x; // total accel in g (rest ≈ 1)
|
|
61
|
+
const y = m.userAcceleration.y + m.gravity.y;
|
|
62
|
+
const z = m.userAcceleration.z + m.gravity.z;
|
|
63
|
+
onAccelMagnitude(Math.sqrt(x * x + y * y + z * z));
|
|
64
|
+
}, 100);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function startAndroidShake(): void {
|
|
68
|
+
const sm = Utils.android
|
|
69
|
+
.getApplicationContext()
|
|
70
|
+
.getSystemService(android.content.Context.SENSOR_SERVICE) as android.hardware.SensorManager;
|
|
71
|
+
const accel = sm.getDefaultSensor(android.hardware.Sensor.TYPE_ACCELEROMETER);
|
|
72
|
+
if (!accel) return;
|
|
73
|
+
const G = 9.80665;
|
|
74
|
+
const listener = new android.hardware.SensorEventListener({
|
|
75
|
+
onAccuracyChanged() {},
|
|
76
|
+
onSensorChanged(e: any) {
|
|
77
|
+
const v = e.values;
|
|
78
|
+
onAccelMagnitude(Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) / G);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
sm.registerListener(listener, accel, android.hardware.SensorManager.SENSOR_DELAY_UI);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── menu + info ──────────────────────────────────────────────────────
|
|
85
|
+
let menuOpen = false;
|
|
86
|
+
async function showDevMenu(): Promise<void> {
|
|
87
|
+
if (menuOpen) return; // sensor keeps firing while the sheet is up — don't stack dialogs
|
|
88
|
+
menuOpen = true;
|
|
89
|
+
try {
|
|
90
|
+
const action = await Dialogs.action({
|
|
91
|
+
title: SHELL_CONFIG.name,
|
|
92
|
+
message: 'Developer menu',
|
|
93
|
+
cancelButtonText: 'Cancel',
|
|
94
|
+
actions: ['App Info', 'Reload'],
|
|
95
|
+
});
|
|
96
|
+
if (action === 'App Info') await showInfo();
|
|
97
|
+
else if (action === 'Reload') reloadWebView();
|
|
98
|
+
} finally {
|
|
99
|
+
menuOpen = false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function showInfo(): Promise<void> {
|
|
104
|
+
// Running web version: prefer what kit.updates reported, else read the page's embedded global
|
|
105
|
+
// directly — so the line shows for any server-loader app exposing __APP_VERSION__, even if its
|
|
106
|
+
// native-kit is too old to ship the updates module.
|
|
107
|
+
const current = webInfo.current || (await readPageVersion());
|
|
108
|
+
const lines = [
|
|
109
|
+
`App: ${SHELL_CONFIG.name}`,
|
|
110
|
+
`ID: ${SHELL_CONFIG.appId}`,
|
|
111
|
+
`Shell: ${SHELL_CONFIG.version} (${SHELL_BUILD})`,
|
|
112
|
+
`Platform: ${isIOS ? 'iOS' : 'Android'} ${Device.osVersion}`,
|
|
113
|
+
`Loader: ${SHELL_CONFIG.loader}`,
|
|
114
|
+
];
|
|
115
|
+
if (SHELL_CONFIG.loader === 'server') lines.push(`Remote: ${safeHost(SHELL_CONFIG.serverUrl)}`);
|
|
116
|
+
|
|
117
|
+
const build = webInfo.build ? ` · build ${webInfo.build}` : '';
|
|
118
|
+
lines.push(`Web version: ${current || 'unknown'}${build}`);
|
|
119
|
+
if (webInfo.latest && current && webInfo.latest !== current) lines.push(`⚠️ Update available: ${webInfo.latest} — Reload to apply`);
|
|
120
|
+
else if (webInfo.latest) lines.push('✓ Up to date');
|
|
121
|
+
|
|
122
|
+
Dialogs.alert({ title: 'App Info', message: lines.join('\n'), okButtonText: 'Close' });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Read the running page's embedded version directly from the WebView: `window.__APP_VERSION__`,
|
|
126
|
+
* then a `<meta name="app-version">` fallback. '' if neither is present (or eval fails). */
|
|
127
|
+
async function readPageVersion(): Promise<string> {
|
|
128
|
+
try {
|
|
129
|
+
const v = await bridge.evalJs(
|
|
130
|
+
'window.__APP_VERSION__ || document.querySelector(\'meta[name="app-version"]\')?.getAttribute("content") || ""'
|
|
131
|
+
);
|
|
132
|
+
return v ? String(v) : '';
|
|
133
|
+
} catch {
|
|
134
|
+
return '';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Host[:port] only — strips scheme, path, query, fragment AND any `user:pass@` userinfo, so no
|
|
139
|
+
* credentials or tokens embedded in the URL ever surface in diagnostics. */
|
|
140
|
+
function safeHost(url: string): string {
|
|
141
|
+
const afterScheme = String(url || '').replace(/^[a-z][a-z0-9+.-]*:\/\//i, '');
|
|
142
|
+
const authority = afterScheme.split(/[/?#]/)[0]; // drop path/query/fragment
|
|
143
|
+
return authority.split('@').pop() || authority; // drop userinfo
|
|
144
|
+
}
|
|
@@ -3,12 +3,13 @@ import { bridge } from './bridge';
|
|
|
3
3
|
import { SHELL_CONFIG } from './config';
|
|
4
4
|
import { onPwaHandshake } from './events';
|
|
5
5
|
import { showToast } from './toast';
|
|
6
|
+
import { showBanner, dismissBanner } from './banner';
|
|
6
7
|
import { setStatusBarStyle } from './status-bar';
|
|
7
8
|
import { buildCapabilityMap } from './capabilities.manifest';
|
|
8
9
|
import { ACTIVE_MODULE_NAMES } from './active-modules.generated';
|
|
9
10
|
|
|
10
11
|
/** Build identifier for the native shell bundle — bump per deploy to spot stale bundles. */
|
|
11
|
-
export const SHELL_BUILD = '
|
|
12
|
+
export const SHELL_BUILD = 'updates-devmenu-2';
|
|
12
13
|
|
|
13
14
|
/** Register all protocol-v1 handlers. */
|
|
14
15
|
export function registerHandlers(): void {
|
|
@@ -23,7 +24,7 @@ export function registerHandlers(): void {
|
|
|
23
24
|
return {
|
|
24
25
|
protocol: 1,
|
|
25
26
|
platform: isIOS ? 'ios' : 'android',
|
|
26
|
-
app: { id: SHELL_CONFIG.appId, name: SHELL_CONFIG.name, version: SHELL_CONFIG.version, build: SHELL_BUILD },
|
|
27
|
+
app: { id: SHELL_CONFIG.appId, name: SHELL_CONFIG.name, version: SHELL_CONFIG.version, build: SHELL_BUILD, loader: SHELL_CONFIG.loader },
|
|
27
28
|
debug: { lastNotifTap: safeJson(ApplicationSettings.getString('kit:__notifTap', '')) },
|
|
28
29
|
capabilities,
|
|
29
30
|
};
|
|
@@ -127,11 +128,36 @@ export function registerHandlers(): void {
|
|
|
127
128
|
showToast(String(message ?? ''), duration ?? 'short')
|
|
128
129
|
);
|
|
129
130
|
|
|
131
|
+
// Persistent, tappable banner (e.g. the remote-update "tap to reload" prompt). Tap emits
|
|
132
|
+
// `toast.action` { id } back to the web side.
|
|
133
|
+
bridge.register('toast.banner', ({ id, message }: { id: string; message: string }) =>
|
|
134
|
+
showBanner({ id: String(id ?? 'banner'), message: String(message ?? '') })
|
|
135
|
+
);
|
|
136
|
+
bridge.register('toast.dismissBanner', () => dismissBanner());
|
|
137
|
+
|
|
138
|
+
// Hard reload the WebView, bypassing cache — used by the update banner + dev menu.
|
|
139
|
+
bridge.register('app.reload', () => reloadWebView());
|
|
140
|
+
|
|
130
141
|
bridge.register('ui.statusBar.setStyle', ({ style }: { style: 'light' | 'dark' }) =>
|
|
131
142
|
setStatusBarStyle(style)
|
|
132
143
|
);
|
|
133
144
|
}
|
|
134
145
|
|
|
146
|
+
/** Reload the attached WebView from origin, bypassing the HTTP cache (iOS `reloadFromOrigin`,
|
|
147
|
+
* Android `clearCache` + `reload`). No-op if no WebView is attached yet. */
|
|
148
|
+
export function reloadWebView(): void {
|
|
149
|
+
const wv = bridge.getWebView();
|
|
150
|
+
if (!wv) return;
|
|
151
|
+
Utils.dispatchToMainThread(() => {
|
|
152
|
+
if (isIOS && wv.ios) {
|
|
153
|
+
(wv.ios as WKWebView).reloadFromOrigin();
|
|
154
|
+
} else if (isAndroid && wv.android) {
|
|
155
|
+
wv.android.clearCache(true);
|
|
156
|
+
wv.android.reload();
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
135
161
|
/** Parse a stored JSON breadcrumb; null if absent/unparseable (diagnostic, never throws). */
|
|
136
162
|
function safeJson(s: string): unknown {
|
|
137
163
|
if (!s) return null;
|
package/src/cli.ts
CHANGED
|
@@ -358,6 +358,7 @@ export const SHELL_CONFIG = {
|
|
|
358
358
|
backendOrigin: ${JSON.stringify(cfg.backendOrigin ?? '')},
|
|
359
359
|
debug: ${JSON.stringify(cfg.debug ?? false)},
|
|
360
360
|
debugLog: ${JSON.stringify(cfg.debugLog ?? '*')},
|
|
361
|
+
devMenu: ${JSON.stringify(cfg.devMenu ?? true)},
|
|
361
362
|
pushIos: ${JSON.stringify(!!cfg.push?.enabled && cfg.push?.ios !== false)},
|
|
362
363
|
pushAndroid: ${JSON.stringify(!!cfg.push?.enabled && cfg.push?.android !== false)},
|
|
363
364
|
};
|
package/src/config.ts
CHANGED
|
@@ -57,6 +57,10 @@ export interface AppwrapConfig {
|
|
|
57
57
|
/** In debug mode, the value written to `localStorage.DEBUG` at startup so the PWA's logger goes
|
|
58
58
|
* verbose (common convention — `'*'` = all, or comma-separated module names). Default `'*'`. */
|
|
59
59
|
debugLog?: string;
|
|
60
|
+
/** Shake-to-open developer menu (App Info / Reload). Default `true` — ON in store builds too, since
|
|
61
|
+
* it only exposes non-sensitive diagnostics (ids, versions, loader, remote host). Set `false` to
|
|
62
|
+
* disable. Remote-update detection (native-kit `kit.updates`) is independent of this flag. */
|
|
63
|
+
devMenu?: boolean;
|
|
60
64
|
/** Apple Development Team ID for device builds (Xcode → Settings → Accounts). */
|
|
61
65
|
teamId?: string;
|
|
62
66
|
/** Path (relative to the PWA project) to a StoreKit configuration file for LOCAL IAP
|
|
@@ -11,7 +11,7 @@ jobs:
|
|
|
11
11
|
- uses: oven-sh/setup-bun@v2
|
|
12
12
|
- run: bun install
|
|
13
13
|
- run: bun test || echo "no tests"
|
|
14
|
-
- run: bun run build
|
|
14
|
+
- run: bun run build # → dist/ (the PWA bundle the wrapper ships)
|
|
15
15
|
|
|
16
16
|
ios-build:
|
|
17
17
|
runs-on: macos-15
|
|
@@ -21,7 +21,7 @@ jobs:
|
|
|
21
21
|
- uses: oven-sh/setup-bun@v2
|
|
22
22
|
- uses: actions/setup-node@v4
|
|
23
23
|
with: { node-version: 22 }
|
|
24
|
-
- run: bun install && bun run build
|
|
24
|
+
- run: bun install && bun run build
|
|
25
25
|
# Pin global tools — unpinned installs can grab a breaking release mid-flight. Bump deliberately.
|
|
26
26
|
- run: npm i -g nativescript@9.0.6
|
|
27
27
|
- run: bunx @livx.cc/appwrap init # native/ is generated (gitignored) — regenerate it fresh in CI
|
|
@@ -29,7 +29,7 @@ jobs:
|
|
|
29
29
|
- uses: actions/setup-java@v4
|
|
30
30
|
with: { distribution: temurin, java-version: 17 }
|
|
31
31
|
- uses: android-actions/setup-android@v3
|
|
32
|
-
- run: bun install && bun run build
|
|
32
|
+
- run: bun install && bun run build # → dist/ (the PWA bundle the wrapper ships)
|
|
33
33
|
# Pin global tools — unpinned installs can grab a breaking release mid-flight. Bump deliberately.
|
|
34
34
|
- run: npm i -g nativescript@9.0.6
|
|
35
35
|
- run: bunx @livx.cc/appwrap init # native/ is generated (gitignored) — regenerate fresh in CI
|
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
- uses: oven-sh/setup-bun@v2
|
|
32
32
|
- uses: actions/setup-node@v4
|
|
33
33
|
with: { node-version: 22 }
|
|
34
|
-
- run: bun install && bun run build
|
|
34
|
+
- run: bun install && bun run build
|
|
35
35
|
# Pin global tools — unpinned installs can grab a breaking release mid-flight. Bump deliberately.
|
|
36
36
|
- run: npm i -g nativescript@9.0.6
|
|
37
37
|
- run: bunx @livx.cc/appwrap init # native/ is generated (gitignored) — regenerate it fresh in CI
|