@wix/himalaya-cli 0.1296.0 → 0.1298.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.
Binary file
package/guides/index.json CHANGED
@@ -95,6 +95,18 @@
95
95
  "category": "Workers & SDKs",
96
96
  "file": "webview-page-sdk.md"
97
97
  },
98
+ {
99
+ "id": "web-page-native-sdk",
100
+ "title": "Call native device features from a web page inside a Himi app",
101
+ "category": "Workers & SDKs",
102
+ "file": "web-page-native-sdk.md"
103
+ },
104
+ {
105
+ "id": "web-page-sdk-reference",
106
+ "title": "Web page SDK reference",
107
+ "category": "Workers & SDKs",
108
+ "file": "web-page-sdk-reference.md"
109
+ },
98
110
  {
99
111
  "id": "component-reference",
100
112
  "title": "Component reference",
@@ -0,0 +1,286 @@
1
+ # Call native device features from a web page inside a Himi app
2
+
3
+ This guide is for anyone — a person or a coding agent such as Base44, Lovable or Claude — building
4
+ a web page (React or plain JavaScript) that will be shown **inside** a Himi mobile app, such as a
5
+ Brandy app that wraps a website. From that page you can call the phone's native features: Health
6
+ (HealthKit / Health Connect), haptics, camera, share, location, secure storage and more.
7
+
8
+ You do **not** install any package. The app injects a small bridge into your page; you paste one
9
+ file (below) that talks to it.
10
+
11
+ ```
12
+ your page → window.HimalayaWorker → the app's companion worker → native iOS / Android
13
+ ```
14
+
15
+ ## Before you start: what the app owner must enable
16
+
17
+ Your page can only use capabilities the **app owner** turned on for your site. In a Brandy app
18
+ that is the `webSdk` block of the app's provisioning file:
19
+
20
+ ```json
21
+ {
22
+ "webSdk": {
23
+ "bundle": "my-site-capabilities",
24
+ "capabilities": ["health", "haptic"],
25
+ "permissionCopy": {
26
+ "health": "Show your daily steps and save the workouts you finish."
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ - `capabilities` — the capability ids your page will call (see the
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.
36
+ - `bundle` — the app's companion worker, which allowlists exactly which methods your page may
37
+ call. A health-only site can omit it; any other set needs one.
38
+
39
+ When you hand work to the app owner, give them the list as `capability.method` pairs, for example
40
+ `health.availability`, `health.requestPermission`, `health.query`, `haptic.play`. A method that is
41
+ not on the allowlist never answers — your call times out (see *Errors*).
42
+
43
+ Your page must be served from the site origin the app was built for. Pages in iframes, and pages
44
+ reached through a redirect to another origin, do not get the bridge.
45
+
46
+ ## Step 1 — add the client file
47
+
48
+ Create `src/lib/himalaya.ts` with exactly this content. It has no dependencies and works with
49
+ React, Vue, Svelte or plain JavaScript callers. It is TypeScript: Vite-based projects (including
50
+ Lovable and Base44) compile it as is; a project with no TypeScript build step needs it compiled to
51
+ JavaScript first (for example with `npx -p typescript tsc --target es2020 --module esnext himalaya.ts`).
52
+
53
+ ```ts
54
+ // himalaya.ts — call native Himi app capabilities from a web page. No dependencies.
55
+ // Protocol v1: the page posts {v, id, capability, method, args}; the app answers {v, id, ok, result | error}.
56
+
57
+ type Bridge = { post(message: unknown): boolean; onMessage(handler: (message: unknown) => void): void };
58
+ type Reply = { v: 1; id: string; ok: boolean; result?: unknown; error?: { message: string; code?: string } };
59
+
60
+ export class HimalayaError extends Error {
61
+ /** "not_in_app" | "bridge_refused" | "timeout", or the capability's own code (e.g. "not_authorized"). */
62
+ readonly code: string;
63
+ constructor(message: string, code: string) {
64
+ super(message);
65
+ this.name = "HimalayaError";
66
+ this.code = code;
67
+ }
68
+ }
69
+
70
+ const pending = new Map<string, { resolve(value: unknown): void; reject(error: Error): void; timer: ReturnType<typeof setTimeout> }>();
71
+ let listeningOn: Bridge | undefined;
72
+ let counter = 0;
73
+
74
+ function bridge(): Bridge | undefined {
75
+ const candidate = (globalThis as { HimalayaWorker?: Bridge }).HimalayaWorker;
76
+ if (!candidate || typeof candidate.post !== "function" || typeof candidate.onMessage !== "function") return undefined;
77
+ if (listeningOn !== candidate) {
78
+ candidate.onMessage((message) => {
79
+ const reply = message as Reply;
80
+ if (!reply || reply.v !== 1 || typeof reply.id !== "string" || typeof reply.ok !== "boolean") return;
81
+ const call = pending.get(reply.id);
82
+ if (!call) return;
83
+ pending.delete(reply.id);
84
+ clearTimeout(call.timer);
85
+ if (reply.ok) call.resolve(reply.result);
86
+ else call.reject(new HimalayaError(reply.error?.message ?? "Native call failed", reply.error?.code ?? "failed"));
87
+ });
88
+ listeningOn = candidate;
89
+ }
90
+ return candidate;
91
+ }
92
+
93
+ /** True when the page is running inside a Himi app WebView (false in a normal browser). */
94
+ export function isInHimiApp(): boolean {
95
+ return bridge() !== undefined;
96
+ }
97
+
98
+ /** Call one native method, e.g. call("health", "query", [{ type: "steps", from, to }]). */
99
+ export function call<T = unknown>(capability: string, method: string, args: unknown[] = [], timeoutMs = 30_000): Promise<T> {
100
+ const target = bridge();
101
+ if (!target) return Promise.reject(new HimalayaError("Not running inside a Himi app", "not_in_app"));
102
+ const id = `page-${Date.now().toString(36)}-${++counter}`;
103
+ return new Promise<T>((resolve, reject) => {
104
+ const timer = setTimeout(() => {
105
+ pending.delete(id);
106
+ reject(new HimalayaError(`${capability}.${method} did not answer — is it enabled for this site?`, "timeout"));
107
+ }, timeoutMs);
108
+ pending.set(id, { resolve: resolve as (value: unknown) => void, reject, timer });
109
+ if (!target.post({ v: 1, id, capability, method, args })) {
110
+ clearTimeout(timer);
111
+ pending.delete(id);
112
+ reject(new HimalayaError("The app refused the message (too large or too frequent)", "bridge_refused"));
113
+ }
114
+ });
115
+ }
116
+
117
+ /** True when `capability.method` answers within `timeoutMs`. Use a read-only method. */
118
+ export async function isEnabled(capability: string, method: string, args: unknown[] = [], timeoutMs = 3_000): Promise<boolean> {
119
+ try {
120
+ await call(capability, method, args, timeoutMs);
121
+ return true;
122
+ } catch (error) {
123
+ // Only an answer from the app (even an error) proves the method is enabled.
124
+ return !(error instanceof HimalayaError && ["timeout", "not_in_app", "bridge_refused"].includes(error.code));
125
+ }
126
+ }
127
+
128
+ type Namespace = { [method: string]: ((...args: any[]) => Promise<any>) & Namespace };
129
+
130
+ function member(capability: string, path: string[]): Namespace {
131
+ return new Proxy(function () {} as unknown as Namespace, {
132
+ get: (_target, key) => (typeof key === "string" && key !== "then" ? member(capability, [...path, key]) : undefined),
133
+ apply: (_target, _this, args: unknown[]) => call(capability, path.join("."), args),
134
+ });
135
+ }
136
+
137
+ /** himalaya.<capability>.<method>(...args), e.g. himalaya.haptic.play("success"). Always returns a Promise. */
138
+ export const himalaya = new Proxy({} as Record<string, Namespace>, {
139
+ get: (_target, key) => (typeof key === "string" && key !== "then" ? member(key, []) : undefined),
140
+ });
141
+ ```
142
+
143
+ Keep it as an ES module (`import` / `export`). In a classic `<script>` tag, do not create a global
144
+ named `himalaya` — the app already defines a read-only `window.himalaya`.
145
+
146
+ ## Step 2 — call a capability
147
+
148
+ Capability ids are snake_case (`health`, `haptic`, `secure_storage`, `local_database`); method
149
+ names are exactly the SDK's function names. Arguments and results are plain JSON.
150
+
151
+ ```ts
152
+ import { himalaya, HimalayaError } from "./lib/himalaya";
153
+
154
+ // Light, fire-and-forget feedback.
155
+ await himalaya.haptic.play("success");
156
+
157
+ // Health: check, ask (from a button tap), then read.
158
+ const { available } = await himalaya.health.availability();
159
+ if (available) {
160
+ await himalaya.health.requestPermission({ read: ["steps"] });
161
+ const midnight = new Date();
162
+ midnight.setHours(0, 0, 0, 0);
163
+ const { samples } = await himalaya.health.query({
164
+ type: "steps",
165
+ from: midnight.toISOString(),
166
+ to: new Date().toISOString(),
167
+ });
168
+ }
169
+ ```
170
+
171
+ `call("health", "availability")` is the same thing without the proxy, and is handy when the
172
+ capability or method name is a variable.
173
+
174
+ ## Step 3 — React pattern
175
+
176
+ ```tsx
177
+ import { useEffect, useState } from "react";
178
+ import { call, himalaya, isInHimiApp, HimalayaError } from "./lib/himalaya";
179
+
180
+ export function StepsCard() {
181
+ const [state, setState] = useState<"checking" | "browser" | "off" | "ready" | "denied" | "failed">("checking");
182
+ const [steps, setSteps] = useState<number | null>(null);
183
+
184
+ useEffect(() => {
185
+ if (!isInHimiApp()) return setState("browser");
186
+ // Answering is not enough: a phone without a health store answers { available: false }.
187
+ call<{ available: boolean }>("health", "availability", [], 3_000)
188
+ .then(({ available }) => setState(available ? "ready" : "off"))
189
+ // Not enabled for this site (timeout) hides the feature; any other failure offers a retry.
190
+ .catch((error) => setState(error instanceof HimalayaError && error.code === "timeout" ? "off" : "failed"));
191
+ }, []);
192
+
193
+ async function connect() {
194
+ try {
195
+ await himalaya.health.requestPermission({ read: ["steps"] });
196
+ const from = new Date(); from.setHours(0, 0, 0, 0);
197
+ const { samples } = await himalaya.health.query({ type: "steps", from: from.toISOString(), to: new Date().toISOString() });
198
+ setSteps(samples.reduce((sum: number, s: { value: number }) => sum + s.value, 0));
199
+ await himalaya.haptic.play("success").catch(() => {});
200
+ } catch (error) {
201
+ // Every outcome gets a state: a click handler that rethrows leaves an unhandled rejection.
202
+ const code = error instanceof HimalayaError ? error.code : "failed";
203
+ setState(code === "not_authorized" ? "denied" : code === "timeout" || code === "unavailable" ? "off" : "failed");
204
+ }
205
+ }
206
+
207
+ if (state === "checking") return null;
208
+ if (state === "browser") return <p>Open this page in our app to see your steps.</p>;
209
+ if (state === "off") return null; // no health store, or Health not enabled for this site — hide it
210
+ if (state === "denied") return <p>Health access is off. You can turn it on in your phone's Settings.</p>;
211
+ if (state === "failed") return <button onClick={connect}>Couldn't load your steps — try again</button>;
212
+ return steps === null ? <button onClick={connect}>Show today's steps</button> : <p>{steps} steps today</p>;
213
+ }
214
+ ```
215
+
216
+ The same page must keep working in an ordinary browser (and in Base44 / Lovable previews): check
217
+ `isInHimiApp()` and render a fallback instead of calling.
218
+
219
+ ## Rules that keep a page working
220
+
221
+ - **Ask for permission only from a user action** (a button tap), after explaining the benefit on
222
+ screen. Never prompt on page load.
223
+ - **Handle "no" and "not available".** Every permission can be denied; Health may not exist on the
224
+ device (`availability()` tells you). Show a calm fallback, not an error.
225
+ - **Hide features the app doesn't enable.** Probe once with `isEnabled(...)` on a read-only method
226
+ and hide the UI when it is `false`. `true` only means the method is enabled — still read its
227
+ answer (`availability()` can say `{ available: false }`).
228
+ - **Plain JSON only.** Arguments and results cross as JSON: no functions, `Date` objects (use ISO
229
+ strings), `Blob`s or class instances. At most 12 arguments; keep each message under 1 MB.
230
+ - **Don't flood the bridge.** About 50 messages per second is the ceiling; debounce sliders and
231
+ scroll handlers.
232
+ - **Request/response only.** Methods that return a stream (`AsyncIterable`) or take a callback are
233
+ not callable from a page — the reference marks them. Poll a read method instead, or ask the
234
+ app owner for a page-friendly method.
235
+ - **Never put secrets in the page.** The page is public web code; native calls are authorized by
236
+ the app, not by anything you send.
237
+
238
+ ## Errors
239
+
240
+ Every failure rejects with a `HimalayaError` that has a `code`:
241
+
242
+ | `code` | Meaning | What to do |
243
+ |---|---|---|
244
+ | `not_in_app` | Ordinary browser or preview, no bridge | Render the browser fallback |
245
+ | `timeout` | No answer: capability or method not enabled for this site, or not on this app build | Hide the feature; ask the app owner to enable `capability.method` |
246
+ | `bridge_refused` | Message too large or too frequent | Send less, less often |
247
+ | `not_authorized` | The user denied permission | Explain how to enable it in Settings |
248
+ | `unavailable` | Not supported on this device | Hide the feature |
249
+ | `invalid_argument` | Wrong argument shape | Fix the call — check the reference |
250
+ | `failed` / other | The native call failed | Show a retry |
251
+
252
+ Codes other than the first three come from the capability itself; its reference page lists them.
253
+
254
+ ## Test it
255
+
256
+ - **In a browser** — `isInHimiApp()` is `false`; confirm your fallback renders.
257
+ - **With a fake bridge** — for unit tests or a design preview, install a fake before your app code
258
+ runs:
259
+
260
+ ```ts
261
+ const answers: Record<string, unknown> = {
262
+ "health.availability": { available: true },
263
+ "health.requestPermission": undefined,
264
+ "health.query": { samples: [{ value: 4200 }] },
265
+ "haptic.play": undefined,
266
+ };
267
+ let reply: (message: unknown) => void = () => {};
268
+ (window as any).HimalayaWorker = {
269
+ post: (m: any) => (setTimeout(() => reply({ v: 1, id: m.id, ok: true, result: answers[`${m.capability}.${m.method}`] })), true),
270
+ onMessage: (handler: (message: unknown) => void) => { reply = handler; },
271
+ };
272
+ ```
273
+
274
+ Never ship the fake: a page that defines `HimalayaWorker` itself is lying to its own feature
275
+ checks.
276
+ - **On a phone** — open the page inside the app build the owner configured. Check each feature
277
+ once with permission granted and once denied.
278
+
279
+ ## Reference
280
+
281
+ - [Web page SDK reference](https://apps.wix.com/_api/himi-serve/docs/web-page-sdk-reference.md) — every capability id and the methods a
282
+ page can call, with signatures.
283
+ - Per-package pages (type definitions and examples; long declarations are abridged there, the
284
+ reference above lists every callable signature in full): `https://apps.wix.com/_api/himi-serve/authoring/resource/sdks%2F<package>`,
285
+ for example [health](https://apps.wix.com/_api/himi-serve/authoring/resource/sdks%2Fhealth) and
286
+ [haptic](https://apps.wix.com/_api/himi-serve/authoring/resource/sdks%2Fhaptic).