@wix/himalaya-cli 0.1321.0 → 0.1322.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.
Files changed (21) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/webview/src/index.d.ts +4 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/webview-page/src/index.d.ts +16 -1
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-v1/src/display.d.ts +21 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/webview-page.mjs +78 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/webview.mjs +16 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-stores.mjs +13 -4
  7. package/dist/cli.mjs +25 -0
  8. package/guides/sdk-reference.json +33 -3
  9. package/guides/web-page-native-sdk.md +154 -33
  10. package/guides/webview-page-sdk.md +11 -0
  11. package/package.json +2 -2
  12. package/templates/beauty-salon/dev/index.ts +492 -0
  13. package/templates/beauty-salon/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
  14. package/templates/branded-lite/dev/index.ts +492 -0
  15. package/templates/branded-lite/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
  16. package/templates/gym-fitness/dev/index.ts +492 -0
  17. package/templates/gym-fitness/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
  18. package/templates/portfolio-creative/dev/index.ts +492 -0
  19. package/templates/portfolio-creative/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
  20. package/templates/retail-store/dev/index.ts +492 -0
  21. package/templates/retail-store/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
@@ -31,19 +31,30 @@ Your page can only use capabilities the **app owner** turned on for your site. I
31
31
 
32
32
  - `capabilities` — the capability ids your page will call (see the
33
33
  [Web page SDK reference](https://apps.wix.com/_api/himi-serve/docs/web-page-sdk-reference.md) for every id and method).
34
- - `permissionCopy` — the sentence the phone shows when it asks the user for permission. Write it
35
- for the end user, about what *they* get.
34
+ - `permissionCopy` — optional: the sentence the phone shows when it asks the user for permission,
35
+ at most 500 characters. Write it for the end user, about what *they* get. Only capabilities that
36
+ prompt need one (Health, camera, photos, location, microphone…); `haptic` and `share` never ask,
37
+ and a capability left out gets the app's built-in generic sentence.
36
38
  - No worker code is needed. Brandy generates the companion worker that answers your page from
37
- `capabilities`, allowing every method the capability's page lists — except
39
+ `capabilities`, allowing **every** method the capability's page lists — except
38
40
  `presigned_upload.putFile`, which reads a device file and is allowed only when `files` is enabled
39
- too. (An owner who wants a
40
- narrower allowlist can still name their own worker in `bundle`.)
41
+ too. So enabling a capability is enough; the owner does not list methods.
41
42
  - Enabling a capability changes the app's native permissions, so the owner ships a **new app
42
43
  build** with it before your page can call it. Until then the call times out.
43
44
 
44
- When you hand work to the app owner, give them the list as `capability.method` pairs, for example
45
- `health.availability`, `health.requestPermission`, `health.query`, `haptic.play`. A method that is
46
- not on the allowlist never answers — your call times out (see *Errors*).
45
+ **You cannot enable anything from the page, or with any public command.** The app owner is
46
+ whoever builds the app (for an app Wix builds for a customer, the team that ships it). Hand them a
47
+ request they can act on without asking you anything back — for example:
48
+
49
+ > Please enable these web page capabilities for `https://www.example.com` in the app's `webSdk`
50
+ > provisioning and ship a new build: `["health", "haptic", "share"]`, with
51
+ > `permissionCopy.health` = "Show your step count for today." The page checks what is enabled and
52
+ > hides the rest, so it is safe to ship before the build.
53
+
54
+ An owner who wrote their own companion worker (named in `webSdk.bundle`) chooses methods one by
55
+ one: for that app only, add the exact `capability.method` pairs your page calls, for example
56
+ `health.availability`, `health.requestPermission`, `health.query`, `haptic.play`. A method that
57
+ worker does not allow never answers — your call times out (see *Errors*).
47
58
 
48
59
  Your page must be served from the site origin the app was built for. Pages in iframes, and pages
49
60
  reached through a redirect to another origin, do not get the bridge.
@@ -100,7 +111,12 @@ export function isInHimiApp(): boolean {
100
111
  return bridge() !== undefined;
101
112
  }
102
113
 
103
- /** Call one native method, e.g. call("health", "query", [{ type: "steps", from, to }]). */
114
+ /**
115
+ * Call one native method, e.g. call("health", "query", [{ type: "steps", from, to }]).
116
+ * A call that shows native UI (a permission prompt, the share sheet) answers only when the user is
117
+ * done: give it a long timeoutMs, such as 120_000. Never wait without a deadline — a message the app
118
+ * drops (see "Keep what you send small") gets no answer at all.
119
+ */
104
120
  export function call<T = unknown>(capability: string, method: string, args: unknown[] = [], timeoutMs = 30_000): Promise<T> {
105
121
  const target = bridge();
106
122
  if (!target) return Promise.reject(new HimalayaError("Not running inside a Himi app", "not_in_app"));
@@ -119,8 +135,86 @@ export function call<T = unknown>(capability: string, method: string, args: unkn
119
135
  });
120
136
  }
121
137
 
122
- /** True when `capability.method` answers within `timeoutMs`. Use a read-only method. */
123
- export async function isEnabled(capability: string, method: string, args: unknown[] = [], timeoutMs = 3_000): Promise<boolean> {
138
+ let manifest: { on: Bridge; list: Record<string, string[]> } | undefined;
139
+
140
+ let asking: { on: Bridge; until: number; answer: Promise<Record<string, string[]> | null> } | undefined;
141
+
142
+ /**
143
+ * ONE question loop per bridge, however many callers wait on it: every message counts against the
144
+ * app's rate limit, so twenty gated controls must not ask twenty times a second. It asks every
145
+ * half second until the latest caller's deadline (a message sent before the app is ready is lost);
146
+ * each question listens for 2 s, so a slow answer still lands and at most four are in flight.
147
+ */
148
+ function askShared(target: Bridge, budget: number): Promise<Record<string, string[]> | null> {
149
+ const until = Date.now() + budget;
150
+ if (asking?.on === target) {
151
+ asking.until = Math.max(asking.until, until);
152
+ return asking.answer;
153
+ }
154
+ const state = { on: target, until, answer: Promise.resolve<Record<string, string[]> | null>(null) };
155
+ asking = state; // before the first ask: a loop that ends at once must clear itself
156
+ state.answer = new Promise((resolve) => {
157
+ let done = false;
158
+ const finish = (list: Record<string, string[]> | null) => {
159
+ if (done) return;
160
+ done = true;
161
+ clearInterval(again);
162
+ if (asking === state) asking = undefined;
163
+ if (list) manifest = { on: target, list };
164
+ resolve(list);
165
+ };
166
+ const ask = () => {
167
+ if (done) return;
168
+ const left = state.until - Date.now();
169
+ if (left <= 0 || bridge() !== target) return finish(null); // deadline, or the page's bridge changed
170
+ call<{ capabilities?: Record<string, string[]> }>("himalaya", "capabilities", [], Math.max(1, Math.min(2_000, left))).then(
171
+ (reply) => finish(reply?.capabilities ?? null),
172
+ // A timed-out question is not an answer; the deadline decides. Anything else is final.
173
+ (error) => { if (!(error instanceof HimalayaError && error.code === "timeout")) finish(null); },
174
+ );
175
+ };
176
+ const again = setInterval(ask, 500);
177
+ ask();
178
+ });
179
+ return state.answer;
180
+ }
181
+
182
+ /**
183
+ * What this app lets the page call, as { capability: [method, …] }. null outside the app, in an app
184
+ * build older than this question, or when nothing answered within timeoutMs. A list is remembered
185
+ * (and returned at once, whatever timeoutMs); a miss is not, because an app that is still starting
186
+ * drops the first messages.
187
+ */
188
+ export async function enabledCapabilities(timeoutMs = 3_000): Promise<Record<string, string[]> | null> {
189
+ const target = bridge();
190
+ if (!target) return null;
191
+ if (manifest?.on === target) return manifest.list;
192
+ if (!(timeoutMs > 0)) return null; // no time to ask: nothing can answer
193
+ // setTimeout cannot wait longer than 2^31-1 ms (a longer delay fires at once), so clamp it.
194
+ const budget = Math.min(timeoutMs, 2_147_483_647);
195
+ const answer = askShared(target, budget);
196
+ // This caller's own deadline; the shared question may keep going for a caller who waits longer.
197
+ return new Promise((resolve) => {
198
+ const stop = setTimeout(() => resolve(manifest?.on === target ? manifest.list : null), budget);
199
+ void answer.then((list) => { clearTimeout(stop); resolve(list); });
200
+ });
201
+ }
202
+
203
+ /**
204
+ * True when the app lets the page call `capability.method`, read from enabledCapabilities(): nothing
205
+ * is called and nothing happens on the phone. false when there is no list (outside the app, an app
206
+ * build older than the list, or no answer yet).
207
+ */
208
+ export async function isEnabled(capability: string, method: string, timeoutMs = 3_000): Promise<boolean> {
209
+ const enabled = await enabledCapabilities(timeoutMs);
210
+ return enabled?.[capability]?.includes(method) ?? false;
211
+ }
212
+
213
+ /**
214
+ * For an app build older than enabledCapabilities(): CALL `capability.method` and report whether the
215
+ * app answered it (even with an error). It runs the method, so pass only a read-only one.
216
+ */
217
+ export async function probe(capability: string, method: string, args: unknown[] = [], timeoutMs = 3_000): Promise<boolean> {
124
218
  try {
125
219
  await call(capability, method, args, timeoutMs);
126
220
  return true;
@@ -154,15 +248,15 @@ Capability ids are snake_case (`health`, `haptic`, `secure_storage`, `local_data
154
248
  names are exactly the SDK's function names. Arguments and results are plain JSON.
155
249
 
156
250
  ```ts
157
- import { himalaya, HimalayaError } from "./lib/himalaya";
251
+ import { call, himalaya, HimalayaError } from "./lib/himalaya";
158
252
 
159
253
  // Light, fire-and-forget feedback.
160
254
  await himalaya.haptic.play("success");
161
255
 
162
- // Health: check, ask (from a button tap), then read.
256
+ // Health: check, ask (from a button tap), then read. The prompt waits for the user: allow two minutes.
163
257
  const { available } = await himalaya.health.availability();
164
258
  if (available) {
165
- await himalaya.health.requestPermission({ read: ["steps"] });
259
+ await call("health", "requestPermission", [{ read: ["steps"] }], 120_000);
166
260
  const midnight = new Date();
167
261
  midnight.setHours(0, 0, 0, 0);
168
262
  const { samples } = await himalaya.health.query({
@@ -174,13 +268,21 @@ if (available) {
174
268
  ```
175
269
 
176
270
  `call("health", "availability")` is the same thing without the proxy, and is handy when the
177
- capability or method name is a variable.
271
+ capability or method name is a variable — or when you need a timeout other than the 30-second
272
+ default. **Give every call that shows native UI a long deadline** — `call(..., 120_000)` for a
273
+ permission prompt, the share sheet, the camera or a photo picker: it answers when the user is
274
+ done, and the 30-second default can turn a slow user into a `timeout` your page reads as "not
275
+ enabled". Keep a deadline even there: a message the app drops gets no answer at all.
276
+
277
+ `steps`, `activeEnergy` and `distance` come back summed (one sample for the whole range, or one
278
+ per `bucket: "hour" | "day"`), and `heartRate` averaged; summing `samples` for a total is right
279
+ either way.
178
280
 
179
281
  ## Step 3 — React pattern
180
282
 
181
283
  ```tsx
182
284
  import { useEffect, useState } from "react";
183
- import { call, himalaya, isInHimiApp, HimalayaError } from "./lib/himalaya";
285
+ import { call, himalaya, isEnabled, isInHimiApp, HimalayaError } from "./lib/himalaya";
184
286
 
185
287
  export function StepsCard() {
186
288
  const [state, setState] = useState<"checking" | "browser" | "off" | "ready" | "denied" | "failed">("checking");
@@ -188,8 +290,10 @@ export function StepsCard() {
188
290
 
189
291
  useEffect(() => {
190
292
  if (!isInHimiApp()) return setState("browser");
191
- // Answering is not enough: a phone without a health store answers { available: false }.
192
- call<{ available: boolean }>("health", "availability", [], 3_000)
293
+ // Enabled for this site? Answered from the app's list, without calling anything.
294
+ isEnabled("health", "availability")
295
+ .then((on) => (on ? call<{ available: boolean }>("health", "availability", [], 3_000) : { available: false }))
296
+ // Enabled is not enough: a phone without a health store answers { available: false }.
193
297
  .then(({ available }) => setState(available ? "ready" : "off"))
194
298
  // Not enabled for this site (timeout) hides the feature; any other failure offers a retry.
195
299
  .catch((error) => setState(error instanceof HimalayaError && error.code === "timeout" ? "off" : "failed"));
@@ -197,7 +301,7 @@ export function StepsCard() {
197
301
 
198
302
  async function connect() {
199
303
  try {
200
- await himalaya.health.requestPermission({ read: ["steps"] });
304
+ await call("health", "requestPermission", [{ read: ["steps"] }], 120_000); // waits for the user
201
305
  const from = new Date(); from.setHours(0, 0, 0, 0);
202
306
  const { samples } = await himalaya.health.query({ type: "steps", from: from.toISOString(), to: new Date().toISOString() });
203
307
  setSteps(samples.reduce((sum: number, s: { value: number }) => sum + s.value, 0));
@@ -232,9 +336,21 @@ The same page must keep working in an ordinary browser (and in Base44 / Lovable
232
336
  device (`availability()` tells you). Show a calm fallback, not an error. On iOS a denied Health
233
337
  **read** is not reported as `not_authorized`: the read succeeds with no samples, exactly like a
234
338
  day with no data. Word an empty result so it also covers "access is off".
235
- - **Hide features the app doesn't enable.** Probe once with `isEnabled(...)` on a read-only method
236
- and hide the UI when it is `false`. `true` only means the method is enabled — still read its
237
- answer (`availability()` can say `{ available: false }`).
339
+ - **Hide features the app doesn't enable.** Check with `isEnabled(capability, method)` and hide the
340
+ UI when it is `false`. The app answers from its list of enabled capabilities
341
+ (`enabledCapabilities()`), so nothing is called and nothing happens on the phone — this works
342
+ for `haptic.play` too. An app build older than that list has none, so `isEnabled` answers
343
+ `false` there. To support such a build, fall back to `probe(capability, method)`, which **calls**
344
+ the method — use it only with a read-only one (`health.availability`, never `haptic.play`).
345
+ `true` only means the method is enabled — still read its answer (`availability()` can say
346
+ `{ available: false }`).
347
+ - **Ask for a permission once, then trust the result you read.** `health.requestPermission`
348
+ resolves with `{ granted }`, but on iOS a read type counts as granted once asked, whatever the
349
+ user chose — so read the data and treat an empty result as "no data or access off". The phone
350
+ shows each permission prompt once; asking again answers without showing anything.
351
+ - **Check for the app when you need it.** In current app builds the bridge exists before your
352
+ first script runs. On an older Android System WebView it is added as the page loads, so call
353
+ `isInHimiApp()` where you use it (for example in `useEffect`), not once at module load.
238
354
  - **Plain JSON only.** Arguments and results cross as JSON: no functions, `Date` objects (use ISO
239
355
  strings), `Blob`s or class instances.
240
356
  - **Keep what you send small.** The app checks every message a page sends and silently drops one
@@ -258,7 +374,7 @@ Every failure rejects with a `HimalayaError` that has a `code`:
258
374
  | `code` | Meaning | What to do |
259
375
  |---|---|---|
260
376
  | `not_in_app` | Ordinary browser or preview, no bridge | Render the browser fallback |
261
- | `timeout` | No answer: capability or method not enabled for this site, not on this app build, or the message broke a size rule (see *Keep what you send small*) | Check your arguments; otherwise hide the feature and ask the app owner to enable `capability.method` |
377
+ | `timeout` | No answer within `timeoutMs`: capability or method not enabled for this site, not on this app build, or the message broke a size rule (see *Keep what you send small*). A call that shows native UI can also time out while the user is still looking at it — give it a long `timeoutMs` (`120_000`) | Check your arguments; otherwise hide the feature and ask the app owner to enable the capability |
262
378
  | `bridge_refused` | The app's bridge refused the message outright | Retry later; send less often |
263
379
  | `not_authorized` | The user denied permission | Explain how to enable it in Settings |
264
380
  | `unavailable` | Not supported on this device | Hide the feature |
@@ -275,6 +391,8 @@ Codes other than the first three come from the capability itself; its reference
275
391
 
276
392
  ```ts
277
393
  const answers: Record<string, unknown> = {
394
+ // What the app answers when the page asks which capabilities it may call.
395
+ "himalaya.capabilities": { v: 1, capabilities: { health: ["availability", "query", "requestPermission"], haptic: ["play"] } },
278
396
  "health.availability": { available: true },
279
397
  "health.requestPermission": undefined,
280
398
  "health.query": { samples: [{ value: 4200 }] },
@@ -289,21 +407,24 @@ Codes other than the first three come from the capability itself; its reference
289
407
 
290
408
  Never ship the fake: a page that defines `HimalayaWorker` itself is lying to its own feature
291
409
  checks.
292
- - **On a phone** — open the page inside the app build the owner configured. Check each feature
293
- once with permission granted and once denied.
410
+ - **On a phone** — there is no public preview app that carries your site's capabilities: ask the
411
+ app owner for an installable test build of the app with your capabilities enabled, open your
412
+ page inside it, and check each feature once with permission granted and once denied. Until then,
413
+ the fake bridge above is the test.
294
414
 
295
415
  ## More recipes
296
416
 
297
417
  Each uses the client from Step 1. The exact types are on each capability's page (see *Reference*).
298
418
 
299
419
  ```ts
300
- import { himalaya, HimalayaError } from "./lib/himalaya";
420
+ import { call, himalaya } from "./lib/himalaya";
301
421
 
302
- // Share a link through the phone's share sheet (from a tap).
303
- const { completed } = await himalaya.share.share({ text: "Join me on Friday", url: "https://example.com/e/42" });
422
+ // Share a link through the phone's share sheet (from a tap). The sheet waits for the user: allow two minutes.
423
+ // Outside the app, fall back to the browser's own share (or copy the link).
424
+ const { completed } = await call<{ completed: boolean }>("share", "share", [{ text: "Join me on Friday", url: "https://example.com/e/42" }], 120_000);
304
425
 
305
426
  // Where am I? Ask first, then read once. getCurrent answers null when no fix arrives in time.
306
- const status = await himalaya.location.requestPermission();
427
+ const status = await call("location", "requestPermission", [], 120_000);
307
428
  if (status === "granted") {
308
429
  const here = await himalaya.location.getCurrent({ timeoutMs: 10_000 });
309
430
  if (here) console.log(here.lat, here.lng, `±${here.accuracy}m`);
@@ -315,7 +436,7 @@ const saved = await himalaya.secure_storage.read("refresh-token"); // string | n
315
436
 
316
437
  // Scan a QR code with the camera (iOS). null when the user cancels — and always on Android,
317
438
  // which has no built-in QR decoder — so keep a way to type the code in.
318
- const code = (await himalaya.camera.scanQR()) ?? window.prompt("Enter the code");
439
+ const code = (await call<string | null>("camera", "scanQR", [], 120_000)) ?? window.prompt("Enter the code");
319
440
  ```
320
441
 
321
442
  **Files arrive as handles.** Methods that return a photo, recording or file give you a
@@ -324,15 +445,15 @@ You cannot put it in an `<img>` directly. Hand it to another call that takes one
324
445
  save it to the photo library:
325
446
 
326
447
  ```ts
327
- const [photo] = await himalaya.photos.pick({ limit: 1 });
328
- if (photo) await himalaya.share.share({ files: [photo] });
448
+ const [photo] = await call<any[]>("photos", "pick", [{ limit: 1 }], 120_000);
449
+ if (photo) await call("share", "share", [{ files: [photo] }], 120_000);
329
450
  ```
330
451
 
331
452
  To get the bytes into the page, read them with the `files` capability (the app owner must enable
332
453
  it too). `files.readBase64` returns the contents as base64, up to 8 MiB:
333
454
 
334
455
  ```ts
335
- const [photo] = await himalaya.photos.pick({ limit: 1 });
456
+ const [photo] = await call<any[]>("photos", "pick", [{ limit: 1 }], 120_000);
336
457
  if (photo) {
337
458
  const base64 = await himalaya.files.readBase64(photo);
338
459
  const img = document.createElement("img");
@@ -70,6 +70,17 @@ logicBundle: {
70
70
  }
71
71
  ```
72
72
 
73
+ Every worker built on `exposeCapabilitiesToPage` also answers the page's handshake,
74
+ `call("himalaya", "capabilities")`, with exactly the allowlist it was given
75
+ (`{ v: 1, capabilities: { health: ["availability", …] } }`); the page client's
76
+ `enabledCapabilities()` and `isEnabled()` (in both the pasted guide client and
77
+ `@wix/himalaya/webview-page`) read it, so a page hides what is off without calling a method or
78
+ waiting out a timeout. They share one question per page, re-asked every 0.5 s until answered or the latest caller's deadline (at most two messages a second, however many controls check), since a worker that is still
79
+ booting drops the first messages. `isEnabled()` never calls the method: with no list (an app built
80
+ before the handshake) it answers `false`, and a page that must support such a build calls
81
+ `probe()` — which does call the method — with a read-only one. `himalaya` is reserved: exposing a capability under that name
82
+ throws.
83
+
73
84
  Do not build one worker that imports every SDK. Static bundle scanning would correctly conclude
74
85
  that the release uses every capability, forcing unrelated permissions and native modules into the
75
86
  binary. One app-specific worker imports exactly the app’s selected subset.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1321.0",
3
+ "version": "0.1322.0",
4
4
  "type": "module",
5
5
  "description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
6
6
  "keywords": [
@@ -71,5 +71,5 @@
71
71
  "artifactId": "himalaya-cli"
72
72
  }
73
73
  },
74
- "falconPackageHash": "3318113ec10ae018bbbe09e99bb3bef1207460516f9ca12d20e128ff"
74
+ "falconPackageHash": "bb7cc4c5d0d0841cd11950b890b11f51d812396f8f4a0649727b0501"
75
75
  }