@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.
- package/dist/cli.mjs +8587 -1667
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/index.json +12 -0
- package/guides/web-page-native-sdk.md +286 -0
- package/guides/web-page-sdk-reference.md +642 -0
- package/guides/webview-page-sdk.md +8 -1
- package/package.json +2 -2
|
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).
|