@livx.cc/appwrap 0.61.19 → 0.61.20

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.19",
3
+ "version": "0.61.20",
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",
@@ -84,7 +84,7 @@ export const SHELL_CONFIG = {
84
84
  * present AND not `enabled:false`); when false the menu action, banner, and boot override are all
85
85
  * inert. `envs` = declared presets; `allowPattern` = anchored regex gating "Other" (default-deny
86
86
  * when ''). Stamped by `appwrap init`/`sync` — see `stampShellConfig`. */
87
- envSwitcher: { enabled: false, envs: [] as { label: string; url: string }[], allowPattern: '' },
87
+ envSwitcher: { enabled: false, envs: [] as { label: string; url: string }[], allowPattern: '', params: [] as { key: string; label: string; options: string[]; optionsUrl: string; optionsPath: string; defaultValue?: string }[] },
88
88
  /** TCC-gated web APIs this build DECLARED (active modules + the config's `permissions{}`) — what the
89
89
  * document-start capability guard exposes to the page. NOT the same question as "is the Info.plist
90
90
  * usage string present": the plist also carries the webview baseline (NSCameraUsageDescription is
@@ -5,6 +5,7 @@ import { OVERRIDE_KEY, effectiveServerUrl, isUrlAllowed } from './server-url';
5
5
  import { bridge } from './bridge';
6
6
  import { refreshEnvBanner } from './env-banner';
7
7
  import { refreshEnvKeepAwake } from './env-keepawake';
8
+ import { paramMenuLabel, showParamPicker, urlParamDefs } from './url-params';
8
9
 
9
10
  /**
10
11
  * Runtime env-switcher — re-point a `loader:'server'` shell between declared environments (prod / lab /
@@ -164,6 +165,8 @@ export async function showEnvSwitcher(): Promise<void> {
164
165
  const allowOther = !!SHELL_CONFIG.envSwitcher?.allowPattern;
165
166
  const actions = envs.map((e) => (e.url === active ? `${e.label} ✓` : e.label));
166
167
  if (allowOther) actions.push('Other…');
168
+ const params = urlParamDefs().map((p) => ({ p, label: paramMenuLabel(p) }));
169
+ actions.push(...params.map((x) => x.label));
167
170
  actions.push('Reset to default');
168
171
 
169
172
  const choice = await Dialogs.action({
@@ -176,6 +179,8 @@ export async function showEnvSwitcher(): Promise<void> {
176
179
 
177
180
  if (choice === 'Reset to default') return void (await applySwitch(null, 'default'));
178
181
  if (choice === 'Other…') return void (await promptOther());
182
+ const param = params.find((x) => x.label === choice);
183
+ if (param) return void (await showParamPicker(param.p, reloadToEffective));
179
184
 
180
185
  const label = choice.replace(/ ✓$/, '');
181
186
  const env = envs.find((e) => e.label === label);
@@ -58,6 +58,42 @@ export function isOverrideAllowed(url: string): boolean {
58
58
  * override against the same allowlist the switcher uses — the native menu isn't the only writer of the
59
59
  * key (any page JS can write it via `kit.storage.set`), so the read side must not trust it blindly. */
60
60
  export function effectiveServerUrl(): string {
61
+ return withUrlParams(effectiveBaseUrl());
62
+ }
63
+
64
+ /** Persisted values for the config-declared URL params (`envSwitcher.params`), `{ key: value }`. */
65
+ export const URL_PARAMS_KEY = 'kit:urlParams';
66
+
67
+ /** The persisted URL-param selections, filtered to keys DECLARED in `envSwitcher.params` (a page can write
68
+ * the key via `kit.storage`, so undeclared keys / non-string values are dropped). {} when disabled. */
69
+ export function storedUrlParams(): Record<string, string> {
70
+ const declared = new Set((SHELL_CONFIG.envSwitcher?.params ?? []).map((p) => p.key));
71
+ if (SHELL_CONFIG.loader !== 'server' || !SHELL_CONFIG.envSwitcher?.enabled || !declared.size) return {};
72
+ try {
73
+ const raw = JSON.parse(ApplicationSettings.getString(URL_PARAMS_KEY, '') || '{}');
74
+ const out: Record<string, string> = {};
75
+ for (const [k, v] of Object.entries(raw ?? {})) if (declared.has(k) && typeof v === 'string' && v) out[k] = v;
76
+ return out;
77
+ } catch {
78
+ return {};
79
+ }
80
+ }
81
+
82
+ /** Apply the stored URL params to a load URL (a key already in the URL is REPLACED, not duplicated). */
83
+ export function withUrlParams(url: string): string {
84
+ const params = Object.entries(storedUrlParams());
85
+ if (!params.length) return url;
86
+ try {
87
+ const u = new URL(url);
88
+ for (const [k, v] of params) u.searchParams.set(k, v);
89
+ return u.toString();
90
+ } catch {
91
+ return url; // unparseable base — load it untouched rather than corrupt it
92
+ }
93
+ }
94
+
95
+ /** The env base URL (override or build default) WITHOUT the URL params — what option sources resolve against. */
96
+ export function effectiveBaseUrl(): string {
61
97
  if (SHELL_CONFIG.loader !== 'server') return SHELL_CONFIG.serverUrl;
62
98
  if (SHELL_CONFIG.envSwitcher?.enabled) {
63
99
  try {
@@ -0,0 +1,86 @@
1
+ import { ApplicationSettings, Dialogs, Http } from '@nativescript/core';
2
+ import { SHELL_CONFIG } from './config';
3
+ import { URL_PARAMS_KEY, effectiveBaseUrl, storedUrlParams } from './server-url';
4
+
5
+ /**
6
+ * Config-driven URL-param menu for the env-switcher (`envSwitcher.params`). Each declared param adds a
7
+ * "<Label>: <current>" entry to the Switch Environment sheet; picking a value persists it (`kit:urlParams`)
8
+ * and reloads the WebView with `?<key>=<value>` appended (see `withUrlParams` in server-url.ts). The first
9
+ * option, "default", means "no param" — or `?<key>=<defaultValue>` when the param declares one. Same gate as the env-switcher itself — no separate trust surface.
10
+ *
11
+ * Options come from a static `options` list and/or `optionsUrl` — a JSON GET resolved against the ACTIVE
12
+ * env's base URL (so a relative path follows an env switch). `optionsPath` (dot path) selects the value
13
+ * inside the response; an array yields its string items, an object yields its keys.
14
+ */
15
+
16
+ export type UrlParamDef = (typeof SHELL_CONFIG.envSwitcher.params)[number];
17
+
18
+ export const DEFAULT_OPTION = 'default';
19
+
20
+ export function urlParamDefs(): UrlParamDef[] {
21
+ return SHELL_CONFIG.envSwitcher?.params ?? [];
22
+ }
23
+
24
+ /** Menu entry label for a param, e.g. "Segment: senior". */
25
+ export function paramMenuLabel(p: UrlParamDef): string {
26
+ return `${p.label || p.key}: ${currentOption(p)}`;
27
+ }
28
+
29
+ /** The menu option currently selected — a stored `defaultValue` reads back as "default". */
30
+ export function currentOption(p: UrlParamDef): string {
31
+ const v = storedUrlParams()[p.key];
32
+ return !v || v === p.defaultValue ? DEFAULT_OPTION : v;
33
+ }
34
+
35
+ /** Extract string options from a JSON response: walk `path`, then array → strings, object → keys. */
36
+ export function extractOptions(json: unknown, path = ''): string[] {
37
+ let v: any = json;
38
+ for (const k of path.split('.').filter(Boolean)) v = v?.[k];
39
+ const list = Array.isArray(v) ? v : v && typeof v === 'object' ? Object.keys(v) : [];
40
+ return list.filter((s): s is string => typeof s === 'string' && !!s);
41
+ }
42
+
43
+ /** Resolve the param's options: "default" + static options + fetched options (deduped). A failed fetch
44
+ * is logged and falls back to the static list — the menu still opens. */
45
+ export async function loadParamOptions(p: UrlParamDef): Promise<string[]> {
46
+ const opts = (p.options ?? []).filter((o) => o !== p.defaultValue);
47
+ if (p.optionsUrl) {
48
+ const url = new URL(p.optionsUrl, effectiveBaseUrl()).toString();
49
+ try {
50
+ const res = await Http.request({ url, method: 'GET', timeout: 8000 });
51
+ if (res.statusCode < 200 || res.statusCode >= 300) throw new Error(`HTTP ${res.statusCode}`);
52
+ opts.push(...extractOptions(res.content?.toJSON(), p.optionsPath));
53
+ } catch (e: any) {
54
+ console.warn(`AppWrap: url-param "${p.key}" options fetch failed (${url}): ${e?.message ?? e}`);
55
+ }
56
+ }
57
+ return [...new Set([DEFAULT_OPTION, ...opts])];
58
+ }
59
+
60
+ /** Persist one param pick. "default" (or '') stores the param's `defaultValue` when declared — an explicit
61
+ * reset sent to the page (e.g. `?segment=default`) — else clears it so the param is omitted. Only an
62
+ * explicit pick is ever sent; an untouched app never gets the defaultValue appended. */
63
+ export function setUrlParam(p: UrlParamDef, value: string): void {
64
+ const next = { ...storedUrlParams() };
65
+ const v = !value || value === DEFAULT_OPTION ? p.defaultValue ?? '' : value;
66
+ if (v) next[p.key] = v;
67
+ else delete next[p.key];
68
+ ApplicationSettings.setString(URL_PARAMS_KEY, JSON.stringify(next));
69
+ }
70
+
71
+ /** Action sheet for one param; on a changed pick, persist + `reload()`. */
72
+ export async function showParamPicker(p: UrlParamDef, reload: () => void): Promise<void> {
73
+ const current = currentOption(p);
74
+ const options = await loadParamOptions(p);
75
+ const choice = await Dialogs.action({
76
+ title: p.label || p.key,
77
+ message: `Current: ${current}`,
78
+ cancelButtonText: 'Cancel',
79
+ actions: options.map((o) => (o === current ? `${o} ✓` : o)),
80
+ });
81
+ if (!choice || choice === 'Cancel') return;
82
+ const value = choice.replace(/ ✓$/, '');
83
+ if (value === current) return;
84
+ setUrlParam(p, value);
85
+ reload();
86
+ }
package/src/cli.ts CHANGED
@@ -975,6 +975,11 @@ export function stampShellConfig(outDir: string, cfg: AppwrapConfig): void {
975
975
  enabled: !!es && es.enabled !== false,
976
976
  envs: (es?.envs ?? []).map((e) => ({ label: String(e.label), url: String(e.url) })),
977
977
  allowPattern: es?.allowPattern ?? '',
978
+ params: (es?.params ?? []).map((p) => ({
979
+ key: String(p.key), label: String(p.label ?? p.key), options: (p.options ?? []).map(String),
980
+ optionsUrl: String(p.optionsUrl ?? ''), optionsPath: String(p.optionsPath ?? ''),
981
+ ...(p.defaultValue ? { defaultValue: String(p.defaultValue) } : {}),
982
+ })),
978
983
  };
979
984
  const hold = cfg.splashHold;
980
985
  const splash = {
@@ -1012,7 +1017,7 @@ export const SHELL_CONFIG = {
1012
1017
  iosKeyboardExtraLift: ${JSON.stringify(cfg.iosKeyboardExtraLift ?? 82)},
1013
1018
  iosHideKeyboardAccessory: ${JSON.stringify(cfg.iosHideKeyboardAccessory ?? false)},
1014
1019
  splash: ${JSON.stringify(splash)} as { hold: boolean; timeoutMs: number; logo: boolean },
1015
- envSwitcher: ${JSON.stringify(envSwitcher)} as { enabled: boolean; envs: { label: string; url: string }[]; allowPattern: string },
1020
+ envSwitcher: ${JSON.stringify(envSwitcher)} as { enabled: boolean; envs: { label: string; url: string }[]; allowPattern: string; params: { key: string; label: string; options: string[]; optionsUrl: string; optionsPath: string; defaultValue?: string }[] },
1016
1021
  webCaps: ${JSON.stringify(webCaps)} as { camera: boolean; microphone: boolean; geolocation: boolean },
1017
1022
  };
1018
1023
  `;
package/src/config.ts CHANGED
@@ -38,6 +38,23 @@ export interface EnvSwitcherConfig {
38
38
  allowPattern?: string;
39
39
  /** Deeplink auto-switch (Phase 2 — not yet implemented). Opt-in. */
40
40
  deeplink?: boolean;
41
+ /** Extra URL query params pickable from the switch menu (same gate). Each adds "<label>: <value>";
42
+ * a pick persists and reloads with `?<key>=<value>` ("default" = param omitted). */
43
+ params?: EnvSwitcherParam[];
44
+ }
45
+
46
+ /** One switchable URL param. Options = static `options` + `optionsUrl` (JSON GET, resolved against the
47
+ * ACTIVE env's URL, so a relative path follows env switches); `optionsPath` dot-path picks the value —
48
+ * an array gives its strings, an object gives its keys. */
49
+ export interface EnvSwitcherParam {
50
+ key: string;
51
+ label?: string;
52
+ options?: string[];
53
+ optionsUrl?: string;
54
+ optionsPath?: string;
55
+ /** Value sent when "default" is picked (e.g. 'default' → `?segment=default`, an explicit reset). Absent →
56
+ * the param is omitted. Only sent after an explicit pick, never on an untouched app. */
57
+ defaultValue?: string;
41
58
  }
42
59
 
43
60
  /** iOS share-extension direct sync (`shareTarget.directSync`). When configured, the generated