@wix/himalaya-cli 0.1320.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.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/app-config.d.ts +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/app-module.d.ts +3 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/biometrics/src/index.d.ts +11 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/catalog-pricing-contract.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/host-session.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/index.d.ts +13 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/mapping-source.d.ts +8 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments.d.ts +5 -4
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/sdk-types/src/capabilities.generated.d.ts +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/webview/src/index.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/webview-page/src/index.d.ts +16 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/tokens.d.ts +9 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/apple-iap-contract.d.ts +5 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/apple-iap-core.d.ts +3 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/checkout-source.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/commerce-contract.d.ts +83 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/commerce-core.d.ts +36 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/commerce.d.ts +8 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/index.d.ts +3 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/purchase-fields.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-v1/src/display.d.ts +21 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/audio-record.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calendar.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calls.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/contacts.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +1205 -71
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +125 -13
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/location.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/motion.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/photos.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdk-types.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/tasks.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/webview-page.mjs +78 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/webview.mjs +16 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-stores.mjs +13 -4
- package/dist/cli.mjs +87 -23
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/getting-started.md +4 -2
- package/guides/himi-skill.md +4 -2
- package/guides/iap-pricing.md +33 -26
- package/guides/index.json +2 -1
- package/guides/sdk-reference.json +50 -8
- package/guides/tokens.md +22 -1
- package/guides/web-page-native-sdk.md +154 -33
- package/guides/webview-page-sdk.md +11 -0
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +492 -0
- package/templates/beauty-salon/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
- package/templates/branded-lite/dev/index.ts +533 -0
- package/templates/branded-lite/tier5-src/_shared/iap-worker.ts +21 -44
- package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +77 -47
- package/templates/branded-lite/tier5-src/himi-branded-lite-programs/index.ts +24 -38
- package/templates/branded-lite/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
- package/templates/gym-fitness/dev/index.ts +492 -0
- package/templates/gym-fitness/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
- package/templates/portfolio-creative/dev/index.ts +492 -0
- package/templates/portfolio-creative/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
- package/templates/retail-store/dev/index.ts +492 -0
- package/templates/retail-store/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
- package/templates/yoga-studio/dev/index.ts +41 -0
- package/templates/yoga-studio/tier5-src/_shared/iap-worker.ts +21 -44
- package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +77 -47
- package/templates/yoga-studio/tier5-src/himi-branded-lite-programs/index.ts +24 -38
package/guides/iap-pricing.md
CHANGED
|
@@ -6,9 +6,11 @@ displays or sells: a program, a product, a plan, or anything else you registered
|
|
|
6
6
|
which items are sold through the App Store, fetches their prices from Apple, and returns display
|
|
7
7
|
state plus a payment route. Your screen binds the result to its own UI.
|
|
8
8
|
|
|
9
|
-
> **
|
|
10
|
-
>
|
|
11
|
-
>
|
|
9
|
+
> **On for bound apps only.** The mapping is read from the app's purchases catalog, which
|
|
10
|
+
> `himi store iap connect` sets up. An app with no purchases-app binding reads every item as
|
|
11
|
+
> `disabled` and keeps its existing price and checkout. This page shows prices and blocks the wrong
|
|
12
|
+
> checkout; selling an App Store item is the shared checkout's job (`createIapCommerce` +
|
|
13
|
+
> `commerce.purchaseFields`, [Wix Apple-IAP checkout](iap-wix-checkout.md#using-it-from-a-screen)).
|
|
12
14
|
> Only one-time purchases (non-consumables) are supported. Subscriptions and consumables are not.
|
|
13
15
|
|
|
14
16
|
## What you get per item
|
|
@@ -20,7 +22,7 @@ what to do:
|
|
|
20
22
|
|---|---|---|
|
|
21
23
|
| `disabled` | Your screen's own price, exactly as before | Allowed |
|
|
22
24
|
| `resolving` | A "Loading price…" label, never your own price | Blocked |
|
|
23
|
-
| `appleIap` | `displayPrice` exactly as Apple formats it, plus a "One-time purchase" note | Blocked;
|
|
25
|
+
| `appleIap` | `displayPrice` exactly as Apple formats it, plus a "One-time purchase" note | Blocked; the shared checkout sells it |
|
|
24
26
|
| `regular` | `displayPrice` (the commerce price you passed), or nothing | Allowed |
|
|
25
27
|
| `unavailable` | "Price unavailable", plus Retry when `retryable` | Blocked |
|
|
26
28
|
|
|
@@ -34,10 +36,9 @@ does not mean the item is free. Only an explicit `regular` route keeps your exis
|
|
|
34
36
|
returns `null` for any other route. It is for a checkout to compare with the product it is asked to
|
|
35
37
|
sell. Do not put it in screen state.
|
|
36
38
|
|
|
37
|
-
> **
|
|
38
|
-
>
|
|
39
|
-
>
|
|
40
|
-
> flow for them.
|
|
39
|
+
> **Buy through the shared checkout only.** `@wix/himalaya/iap-checkout` (`createIapCommerce`) is
|
|
40
|
+
> the App Store checkout built on this layer, and it sells only in a bound app. Keep App Store items
|
|
41
|
+
> blocked in your own checkout as described above. Never build your own purchase flow for them.
|
|
41
42
|
|
|
42
43
|
## Catalog references
|
|
43
44
|
|
|
@@ -52,13 +53,16 @@ when it registers the item with Apple:
|
|
|
52
53
|
|
|
53
54
|
## Add it to a worker
|
|
54
55
|
|
|
55
|
-
This is a complete worker for one screen
|
|
56
|
+
This is a complete worker for one screen that shows an App Store price and guards its existing
|
|
57
|
+
checkout. Copy the pattern, not the course names. A screen that also SELLS the item passes its
|
|
58
|
+
vertical adapter to `createIapCommerce` and binds Get access with `commerce.purchaseFields` instead of
|
|
59
|
+
blocking ([checkout recipe](iap-wix-checkout.md#using-it-from-a-screen)).
|
|
56
60
|
|
|
57
61
|
```ts
|
|
58
62
|
// catalog-pricing-worker.ts
|
|
59
63
|
import { defineService } from "@himalaya/state";
|
|
60
|
-
import {
|
|
61
|
-
import {
|
|
64
|
+
import { iapPriceFields, onlineProgramItem } from "@himalaya/iap";
|
|
65
|
+
import { createIapCommerce } from "@himalaya/iap-checkout";
|
|
62
66
|
|
|
63
67
|
const COPY = {
|
|
64
68
|
loading: "Loading price…",
|
|
@@ -74,25 +78,25 @@ type CourseState = Record<string, unknown>;
|
|
|
74
78
|
|
|
75
79
|
/** `startCheckout` is the screen's existing commerce flow. */
|
|
76
80
|
export function registerCourseScreen(course: Course, startCheckout: () => Promise<string>) {
|
|
77
|
-
|
|
81
|
+
// No adapters: this screen shows the price and keeps its own checkout. `commerce.pricing` is the
|
|
82
|
+
// shared `createIapCatalogPricing` layer; the host scopes its lookup (app identity, platform, and
|
|
83
|
+
// the app's purchases-app site when it has one).
|
|
84
|
+
const commerce = createIapCommerce({ onChange: () => { void service.publish(); } });
|
|
85
|
+
const pricing = commerce.pricing;
|
|
78
86
|
const item = onlineProgramItem({ id: course.id }, course.commercePrice)!;
|
|
79
87
|
let message = "";
|
|
80
88
|
let scopeIncomplete = false;
|
|
81
89
|
|
|
82
|
-
async function hostScope() {
|
|
83
|
-
const info = await appInfo().catch(() => undefined);
|
|
84
|
-
scopeIncomplete = !info?.appId || !info?.bundleId || !info?.platform;
|
|
85
|
-
return { scope: { appId: info?.appId ?? "", bundleId: info?.bundleId ?? "" }, platform: info?.platform ?? "" };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
90
|
async function loadPrices() {
|
|
89
|
-
if (pricing.enabled)
|
|
91
|
+
if (pricing.enabled) scopeIncomplete = !(await commerce.loadPrices([item])).scopeComplete;
|
|
90
92
|
void service.publish();
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
function present(state: CourseState): CourseState {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
const view = pricing.view(item.reference);
|
|
97
|
+
// `disabled`: no source, or an app that sells nothing through the App Store — the screen's own price.
|
|
98
|
+
if (view.route === "disabled") return { ...state, priceText: course.commercePrice, buyDisabled: false, message };
|
|
99
|
+
const price = iapPriceFields(view, COPY);
|
|
96
100
|
return { ...state, priceText: price.priceText, priceNote: price.priceNote, hidePriceRetry: price.hidePriceRetry && !scopeIncomplete,
|
|
97
101
|
buyDisabled: price.actionDisabled, buyNote: price.actionNote, message };
|
|
98
102
|
}
|
|
@@ -125,14 +129,14 @@ export function registerCourseScreen(course: Course, startCheckout: () => Promis
|
|
|
125
129
|
}
|
|
126
130
|
```
|
|
127
131
|
|
|
128
|
-
Bind the fields in the screen descriptor and declare
|
|
132
|
+
Bind the fields in the screen descriptor and declare the capabilities the worker calls. To show
|
|
129
133
|
real App Store prices, the app must also grant `iap` (and `app_info`) in its `config.ts`
|
|
130
134
|
`capabilities`, so its native shell registers the store bridge; until it does, the screen fails
|
|
131
135
|
closed to "Price unavailable" rather than showing a price it could not confirm.
|
|
132
136
|
|
|
133
137
|
```json
|
|
134
138
|
{
|
|
135
|
-
"logicBundle": { "declaredCapabilities": ["iap", "app_info"] },
|
|
139
|
+
"logicBundle": { "declaredCapabilities": ["iap", "app_info", "wix_api"] },
|
|
136
140
|
"price": { "type": "Text", "props": { "text": "$service:course.priceText", "collapseWhenEmpty": true } },
|
|
137
141
|
"note": { "type": "Text", "props": { "text": "$service:course.priceNote", "collapseWhenEmpty": true } },
|
|
138
142
|
"retry": { "type": "Button", "props": { "title": "Try again", "action": { "actionId": "tier5.course.refreshPrice" } },
|
|
@@ -163,8 +167,9 @@ closed to "Price unavailable" rather than showing a price it could not confirm.
|
|
|
163
167
|
- **Keep prices out of hydration** with `notHydrated`, so a cold start never shows an old amount.
|
|
164
168
|
- **Content first.** A pricing failure is per item. Never replace the screen with an error because a
|
|
165
169
|
price could not load.
|
|
166
|
-
- **iOS only.**
|
|
167
|
-
|
|
170
|
+
- **iOS only.** Android, web and macOS binaries carry no purchases-app binding, so the shipped
|
|
171
|
+
source reads every item there as `disabled` and the screen's own price and checkout apply. A
|
|
172
|
+
mapping that does name an App Store product on those hosts comes back `unavailable`.
|
|
168
173
|
|
|
169
174
|
## Adopting it
|
|
170
175
|
|
|
@@ -173,7 +178,9 @@ closed to "Price unavailable" rather than showing a price it could not confirm.
|
|
|
173
178
|
- **Existing screens, including apps created from a template before this existed:** an SDK update
|
|
174
179
|
never changes a screen that does not call this layer. Add the worker calls, the bindings and the
|
|
175
180
|
two capabilities above. After that, screens pick up layer fixes on their next build.
|
|
176
|
-
- **Custom screens:** follow the worker above for any item you
|
|
181
|
+
- **Custom screens:** follow the worker above for any item you price, and the
|
|
182
|
+
[checkout recipe](iap-wix-checkout.md#using-it-from-a-screen) for any item you sell, whatever its
|
|
183
|
+
vertical.
|
|
177
184
|
- **New item types:** use the kind `himi store iap` registers for it, build `{ kind, id }`, and call
|
|
178
185
|
the same layer. Never write your own mapping lookup, App Store price request or payment guard.
|
|
179
186
|
|
package/guides/index.json
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
],
|
|
21
21
|
"guardrails": [
|
|
22
22
|
"Prefer Tier-5 (SDUI descriptor + JS worker) over native screens — reach for native only when SDUI genuinely can't express it.",
|
|
23
|
-
"A screen that shows or sells an item that may be an App Store in-app purchase (a program, product or plan — template or custom screen alike) uses the shared layer `createIapCatalogPricing` from `@himalaya/iap` (`himi docs iap-pricing`): it batches the mapping lookup, fetches Apple prices and decides the payment route. Never hand-wire a mapping, an Apple price request or a payment guard in a worker. Call `checkoutGuard` immediately before a commerce flow starts, and `continueCheckoutGuard` before each later step of a flow already under way (a payment after a join), so a routine price refresh cannot strand it halfway. It is off until Wix connects the mapping service
|
|
23
|
+
"A screen that shows or sells an item that may be an App Store in-app purchase (a program, product or plan — template or custom screen alike) uses the shared layer `createIapCatalogPricing` from `@himalaya/iap` (`himi docs iap-pricing`): it batches the mapping lookup, fetches Apple prices and decides the payment route. Never hand-wire a mapping, an Apple price request or a payment guard in a worker. Call `checkoutGuard` immediately before a commerce flow starts, and `continueCheckoutGuard` before each later step of a flow already under way (a payment after a join), so a routine price refresh cannot strand it halfway. It is off until Wix connects the mapping service.",
|
|
24
|
+
"To SELL an App Store item, a screen composes `createIapCommerce` from `@himalaya/iap-checkout` once per worker (its catalog pricing plus the shared checkout, with the vertical's order adapter) and binds its Get access action with `commerce.purchaseFields`: the order, Apple's sheet, proof delivery, finishing and the access read all happen inside it. Never call StoreKit (`purchaseForVerification`), `finishTransaction` or the payment client from a worker, never fall back to Wix checkout for an App Store item, and show access only when it reports `accessReady`. Purchasing stays unavailable until Wix provides its routes and mapping.",
|
|
24
25
|
"Keep structure descriptor-authored: use FragmentSlot for enumerable variants and bound list/grid templates for repetition. Use WorkerSubtree only for runtime-unknowable structure, with an explicit screen allowlist, policy budgets, and shipped fallback.",
|
|
25
26
|
"Bindings ($service:<svc>.<path>, $item.<field>, $flag:<key>) are plain strings in ANY prop — INCLUDING Modifiers token refs (background, color, tint), so a worker can drive per-item theming, not just text. They CANNOT target strict-enum fields (style, size, alignment, severity, presentation, …) — those fail at render, not validation.",
|
|
26
27
|
"Workers are sandboxed: no DOM, no timers, no raw fetch. Produce state synchronously in actions; do not rely on an ambient refresh tick.",
|
|
@@ -432,8 +432,8 @@
|
|
|
432
432
|
{
|
|
433
433
|
"name": "BiometricKind",
|
|
434
434
|
"kind": "type",
|
|
435
|
-
"jsdoc": "",
|
|
436
|
-
"decl": "export type BiometricKind = \"face\" | \"fingerprint\" | \"iris\" | \"none\";"
|
|
435
|
+
"jsdoc": "/**\n * The enrolled biometric, as far as the platform can tell:\n * - `\"face\"` / `\"fingerprint\"` / `\"iris\"` — that modality.\n * - `\"biometric\"` — a biometric is enrolled but the platform can't say which\n * (Android phones with both face and fingerprint hardware; the web). Label\n * the unlock neutrally (\"Use biometrics\") rather than guessing a modality.\n * - `\"none\"` — nothing enrolled, or no hardware.\n *\n * Additive: treat any value you don't recognise like `\"biometric\"`.\n */",
|
|
436
|
+
"decl": "export type BiometricKind = \"face\" | \"fingerprint\" | \"iris\" | \"biometric\" | \"none\";"
|
|
437
437
|
},
|
|
438
438
|
{
|
|
439
439
|
"name": "available",
|
|
@@ -1878,7 +1878,7 @@
|
|
|
1878
1878
|
"name": "IapMappingSource",
|
|
1879
1879
|
"kind": "interface",
|
|
1880
1880
|
"jsdoc": "/** A deployed Wix→Apple mapping backend. Never supplied by a screen. */",
|
|
1881
|
-
"decl": "export interface IapMappingSource {\n mapping: PricingMappingProvider;\n /** The site this backend serves, used when the caller passes no site. */\n siteId?: string;\n}"
|
|
1881
|
+
"decl": "export interface IapMappingSource {\n mapping: PricingMappingProvider;\n /** Whether this app sells through the App Store at all — the binary carries a purchases-app binding.\n * When it answers false, every item reads `disabled` with no lookup, exactly as with no source, so the\n * screen's own commerce applies; asked again on every load. Absent means always active. */\n active?(): Promise<boolean>;\n /** The site this backend serves, used when the caller passes no site. */\n siteId?: string;\n}"
|
|
1882
1882
|
},
|
|
1883
1883
|
{
|
|
1884
1884
|
"name": "IapPollDirective",
|
|
@@ -2065,7 +2065,7 @@
|
|
|
2065
2065
|
{
|
|
2066
2066
|
"name": "createWixIapClient",
|
|
2067
2067
|
"kind": "function",
|
|
2068
|
-
"jsdoc": "/**\n * Creates a client for Wix's in-app-purchase payments service (`mobile-apps-wix-iap`), which pays\n * a Wix order with a store purchase the member makes in the app.\n *\n *
|
|
2068
|
+
"jsdoc": "/**\n * Creates a client for Wix's in-app-purchase payments service (`mobile-apps-wix-iap`), which pays\n * a Wix order with a store purchase the member makes in the app.\n *\n * The shared checkout (`createIapCommerce`) builds it with Serve's IAP route for an app whose binary\n * carries a purchases-app binding; `route` stays a parameter with no default. It calls\n * StartPayment (`POST …/payments/start`), CompletePayment (`POST …/payments/complete`)\n * and GetPayment (`GET …/payments/{id}`) through the\n * `wix_api` bridge. A matched or approved Payment is not durable access and never finishes the\n * store transaction.\n *\n * A sale needs the binding baked into the binary and an active mapping for the item; without them\n * nothing is offered, and before a production Serve release carries the IAP routes the mapping read\n * fails, so nothing is offered either. When this client uses the host\n * bridge, it also requires the host's identity clock and passes that captured expectation\n * through the bridge. An injected transport keeps the previous call shape unless the session\n * itself exposes an identity.\n */",
|
|
2069
2069
|
"decl": "export function createWixIapClient(deps: WixIapClientDependencies): WixIapClient;",
|
|
2070
2070
|
"verified": false
|
|
2071
2071
|
},
|
|
@@ -2079,7 +2079,7 @@
|
|
|
2079
2079
|
"name": "HostIdentitySource",
|
|
2080
2080
|
"kind": "interface",
|
|
2081
2081
|
"jsdoc": "/** A session provider that can also name the principal captured for one generation. */",
|
|
2082
|
-
"decl": "export interface HostIdentitySource {\n readonly identityBound: boolean;\n identity(session: SessionSnapshot): HostCallExpectation | null;\n}"
|
|
2082
|
+
"decl": "export interface HostIdentitySource {\n readonly identityBound: boolean;\n identity(session: SessionSnapshot): HostCallExpectation | null;\n /** The site the native bridge fences protected calls to, from the latest ready snapshot. Null\n * when the host installed no session source: iOS publishes the bridge (`identityBound`) to\n * every worker that has `wix_api`, with or without one. */\n site(): string | null;\n}"
|
|
2083
2083
|
},
|
|
2084
2084
|
{
|
|
2085
2085
|
"name": "HostSessionProvider",
|
|
@@ -2248,6 +2248,18 @@
|
|
|
2248
2248
|
"kind": "const",
|
|
2249
2249
|
"jsdoc": "/** The managed native verbs as the delivery coordinator's store. */",
|
|
2250
2250
|
"decl": "export const appleManagedStore: TransactionStore = { purchaseForVerification, getUnfinishedTransactions, finishTransaction };"
|
|
2251
|
+
},
|
|
2252
|
+
{
|
|
2253
|
+
"name": "hostAppBindingSource",
|
|
2254
|
+
"kind": "function",
|
|
2255
|
+
"jsdoc": "/** The purchases-service binding the host baked into this binary at build time (the app's build\n * contract → Info.plist), checked natively against the running bundle, app and site. Never read\n * from a screen, OTA config or navigation param: a worker cannot choose which purchases app it\n * sells through. Null means not provisioned: an unbound build, an older binary without the verb,\n * or a platform with no managed App Store purchasing. A host that has the verb and fails to answer\n * proves neither, so that read rejects and is not kept: the caller must not fall back to the\n * unbound app's commerce. The binding cannot change while the process runs, so a source asks the\n * host once. */",
|
|
2256
|
+
"decl": "export function hostAppBindingSource(): AppBindingSource;"
|
|
2257
|
+
},
|
|
2258
|
+
{
|
|
2259
|
+
"name": "sharedHostAppBindingSource",
|
|
2260
|
+
"kind": "function",
|
|
2261
|
+
"jsdoc": "/** One binding source per worker, shared by pricing and checkout, so one screen never acts on two\n * different answers to the same question. */",
|
|
2262
|
+
"decl": "export function sharedHostAppBindingSource(): AppBindingSource;"
|
|
2251
2263
|
}
|
|
2252
2264
|
]
|
|
2253
2265
|
},
|
|
@@ -4176,6 +4188,18 @@
|
|
|
4176
4188
|
"jsdoc": "",
|
|
4177
4189
|
"decl": "export interface PageCapabilityResponse {\n v: 1;\n id: string;\n ok: boolean;\n result?: unknown;\n error?: { message: string; code?: string };\n}"
|
|
4178
4190
|
},
|
|
4191
|
+
{
|
|
4192
|
+
"name": "PAGE_HANDSHAKE_CAPABILITY",
|
|
4193
|
+
"kind": "const",
|
|
4194
|
+
"jsdoc": "/** `call(\"himalaya\", \"capabilities\")` answers `{ v: 1, capabilities: { <id>: [<method>…] } }`:\n * exactly the allowlist this worker was given. Reserved, so no real capability can take it. */",
|
|
4195
|
+
"decl": "export const PAGE_HANDSHAKE_CAPABILITY = \"himalaya\";"
|
|
4196
|
+
},
|
|
4197
|
+
{
|
|
4198
|
+
"name": "PAGE_HANDSHAKE_METHOD",
|
|
4199
|
+
"kind": "const",
|
|
4200
|
+
"jsdoc": "",
|
|
4201
|
+
"decl": "export const PAGE_HANDSHAKE_METHOD = \"capabilities\";"
|
|
4202
|
+
},
|
|
4179
4203
|
{
|
|
4180
4204
|
"name": "ExposedPageCapability",
|
|
4181
4205
|
"kind": "interface",
|
|
@@ -4228,9 +4252,27 @@
|
|
|
4228
4252
|
{
|
|
4229
4253
|
"name": "call",
|
|
4230
4254
|
"kind": "function",
|
|
4231
|
-
"jsdoc": "/** Invoke one method through the companion worker. Primarily useful to generated bindings. */",
|
|
4255
|
+
"jsdoc": "/** Invoke one method through the companion worker. Primarily useful to generated bindings.\n * A call that shows native UI (a permission prompt, the share sheet) answers only when the user\n * is done with it: pass a long `timeoutMs` (e.g. 120_000) rather than the 30-second default. */",
|
|
4232
4256
|
"decl": "export function call<T>(\n capability: string,\n method: string,\n args: unknown[] = [],\n timeoutMs = 30_000,\n): Promise<T>;"
|
|
4233
4257
|
},
|
|
4258
|
+
{
|
|
4259
|
+
"name": "enabledCapabilities",
|
|
4260
|
+
"kind": "function",
|
|
4261
|
+
"jsdoc": "/**\n * The capabilities and methods this page's companion worker answers, as `{ <id>: [<method>…] }`;\n * `null` outside the app, when the app is older than the handshake, or nothing answered in\n * `timeoutMs`. A list is remembered (and returned at once, whatever `timeoutMs`); a miss is not,\n * because a worker that is still booting drops the first messages.\n */",
|
|
4262
|
+
"decl": "export async function enabledCapabilities(timeoutMs = 3_000): Promise<Record<string, string[]> | null>;"
|
|
4263
|
+
},
|
|
4264
|
+
{
|
|
4265
|
+
"name": "isEnabled",
|
|
4266
|
+
"kind": "function",
|
|
4267
|
+
"jsdoc": "/** Whether the companion worker lets this page call `capability.method`, read from\n * enabledCapabilities() — nothing is called. `false` when there is no list. */",
|
|
4268
|
+
"decl": "export async function isEnabled(capability: string, method: string, timeoutMs = 3_000): Promise<boolean>;"
|
|
4269
|
+
},
|
|
4270
|
+
{
|
|
4271
|
+
"name": "probe",
|
|
4272
|
+
"kind": "function",
|
|
4273
|
+
"jsdoc": "/** For an app older than the handshake: CALL `capability.method` and report whether it answered\n * (even with an error). It runs the method, so pass only a read-only one. */",
|
|
4274
|
+
"decl": "export async function probe(capability: string, method: string, args: unknown[] = [], timeoutMs = 3_000): Promise<boolean>;"
|
|
4275
|
+
},
|
|
4234
4276
|
{
|
|
4235
4277
|
"name": "himalaya",
|
|
4236
4278
|
"kind": "const",
|
|
@@ -23096,12 +23138,12 @@
|
|
|
23096
23138
|
"name": "StoreProductSearch",
|
|
23097
23139
|
"kind": "interface",
|
|
23098
23140
|
"jsdoc": "/** User-facing product discovery. V3 name search/sorting belong to Search\n * Products; Query Products deliberately does not support the name field.\n * V1 uses its documented JSON-string filter/sort dialect. */",
|
|
23099
|
-
"decl": "export interface StoreProductSearch {\n query?: string;\n sort?: 'default' | 'az' | 'za';\n ids?: string[];\n limit?: number;\n cursor?: string;\n /** Extra v3 fieldsets. Additive — widens the default, never replaces it. */\n fields?: readonly string[];\n}"
|
|
23141
|
+
"decl": "export interface StoreProductSearch {\n query?: string;\n sort?: 'default' | 'az' | 'za';\n ids?: string[];\n /**\n * Only products in this category (V3) / collection (V1) — a category screen's\n * \"products in X\". ANDed with `ids` when both are given.\n *\n * V3 matches `allCategoriesInfo.categories`, so a parent category also lists its\n * subcategories' products, as the site does. Only Search Products can filter by\n * category — Query Products answers `not declared as filterable` — so a\n * `categoryId` always routes the V3 read to `/products/search`. V1 matches\n * `collections.id`.\n */\n categoryId?: string;\n limit?: number;\n cursor?: string;\n /** Extra v3 fieldsets. Additive — widens the default, never replaces it. */\n fields?: readonly string[];\n}"
|
|
23100
23142
|
},
|
|
23101
23143
|
{
|
|
23102
23144
|
"name": "searchStoreProducts",
|
|
23103
23145
|
"kind": "function",
|
|
23104
|
-
"jsdoc": "",
|
|
23146
|
+
"jsdoc": "/**\n * Search, sort, and filter products on whichever catalog this site runs — the read\n * behind a shop list, a saved-products list, and a category screen.\n *\n * `categoryId` narrows to one category's products (V3 `allCategoriesInfo`, so a parent\n * category includes its subcategories; V1 `collections.id`), ANDed with `ids`. An\n * empty `ids` array resolves to an empty page WITHOUT a request. Paging is one\n * contract on both catalogs: pass back `pagingMetadata.cursors.next` as `cursor`.\n * A non-catalog failure (401/403, offline) rejects; it is never retried on V1.\n */",
|
|
23105
23147
|
"decl": "export async function searchStoreProducts(options: StoreProductSearch = {}): Promise<StoreProductsResponse>;",
|
|
23106
23148
|
"http": "POST /stores/v1/products/query"
|
|
23107
23149
|
},
|
package/guides/tokens.md
CHANGED
|
@@ -18,7 +18,7 @@ one registry, mirrored byte-for-byte by the iOS + Android decoders.
|
|
|
18
18
|
| `motions` | name → { duration, easing?, spring? } | optional |
|
|
19
19
|
| `materials` | name → { style, tint?, opacity?, blur? } | optional |
|
|
20
20
|
| `tabBar` / `drawer` / `sidebar` | nav-chrome style blocks | optional |
|
|
21
|
-
| `kit` | { material?, variant?, density?, motion? } | optional — the active design kit |
|
|
21
|
+
| `kit` | { material?, variant?, density?, motion?, reduceMotion?, colorScheme? } | optional — the active design kit |
|
|
22
22
|
| `contentMaxWidth` | { base, medium?, expanded? } | optional |
|
|
23
23
|
| `elevations` | name → { level?, tint?, shadow?, border? } | optional — a Card wears `card` |
|
|
24
24
|
| `controls` | { button?: { sm?, md?, lg?, variants? } } | optional — every Button's size and paint |
|
|
@@ -39,6 +39,27 @@ platform's own UI face — SF Pro on iOS/macOS, Roboto on Android, the system UI
|
|
|
39
39
|
A color is a hex string, OR a per-scheme object `{ light, dark?, highContrast? }`. In the
|
|
40
40
|
object form **`light` is required**; `dark`/`highContrast` fall back to it.
|
|
41
41
|
|
|
42
|
+
## Light or dark — `kit.colorScheme`
|
|
43
|
+
|
|
44
|
+
By default an app follows the device appearance. To pin it, set `kit.colorScheme` — it ships
|
|
45
|
+
OTA with the tokens, so a published release re-skins installed apps without a new binary:
|
|
46
|
+
|
|
47
|
+
| value | effect |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `"system"` (or absent) | follow the device light/dark setting |
|
|
50
|
+
| `"light"` | always light |
|
|
51
|
+
| `"dark"` | always dark |
|
|
52
|
+
|
|
53
|
+
Only those three lowercase values are recognised: another string counts as `"system"` and
|
|
54
|
+
`himi validate` warns; a non-string (a number, a boolean) is an error, because it fails the
|
|
55
|
+
whole tokens.json on device. The pin replaces only the device light/dark signal: an explicit
|
|
56
|
+
in-app theme choice (`theme.set`, a host's own Appearance setting), a preview's scheme pin,
|
|
57
|
+
and the OS high-contrast setting still win over it. Native chrome — the status bar and the
|
|
58
|
+
navigation bar — follows the pin too. Binaries older than the core release that added it
|
|
59
|
+
ignore the key and keep following the device. Set it in the top-level `kit` block (an idiom
|
|
60
|
+
overlay cannot change it), merged into the existing block rather than replacing it; adopting a
|
|
61
|
+
design kit keeps it.
|
|
62
|
+
|
|
42
63
|
## Motion
|
|
43
64
|
|
|
44
65
|
`easing` is `linear`, `easeIn`, `easeOut`, `easeInOut`, `spring` or `emphasized` (Material 3's
|
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
123
|
-
|
|
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
|
|
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
|
-
//
|
|
192
|
-
|
|
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
|
|
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.**
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
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** —
|
|
293
|
-
|
|
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 {
|
|
420
|
+
import { call, himalaya } from "./lib/himalaya";
|
|
301
421
|
|
|
302
|
-
// Share a link through the phone's share sheet (from a tap).
|
|
303
|
-
|
|
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
|
|
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
|
|
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
|
|
328
|
-
if (photo) await
|
|
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
|
|
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");
|