@wix/himalaya-cli 0.1319.0 → 0.1321.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 (51) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/app-config.d.ts +1 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/app-module.d.ts +3 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/biometrics/src/index.d.ts +11 -1
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/catalog-pricing-contract.d.ts +4 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/host-session.d.ts +4 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/index.d.ts +13 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/mapping-source.d.ts +8 -3
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments.d.ts +5 -4
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/sdk-types/src/capabilities.generated.d.ts +1 -1
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/tokens.d.ts +9 -1
  11. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/apple-iap-contract.d.ts +5 -0
  12. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/apple-iap-core.d.ts +3 -1
  13. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/checkout-source.d.ts +4 -0
  14. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/commerce-contract.d.ts +83 -0
  15. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/commerce-core.d.ts +36 -0
  16. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/commerce.d.ts +8 -0
  17. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/index.d.ts +3 -0
  18. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/purchase-fields.d.ts +4 -0
  19. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/audio-record.mjs +1 -1
  20. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calendar.mjs +1 -1
  21. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calls.mjs +1 -1
  22. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/contacts.mjs +1 -1
  23. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +1205 -71
  24. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +125 -13
  25. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/location.mjs +1 -1
  26. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/motion.mjs +1 -1
  27. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/photos.mjs +1 -1
  28. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdk-types.mjs +1 -1
  29. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +1 -0
  30. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/tasks.mjs +1 -1
  31. package/dist/cli.mjs +164 -56
  32. package/dist/preview-assets.generated.bin +0 -0
  33. package/guides/capabilities.md +17 -0
  34. package/guides/designing-himi-screens.md +2 -0
  35. package/guides/getting-started.md +4 -2
  36. package/guides/himi-skill.md +4 -2
  37. package/guides/iap-pricing.md +33 -26
  38. package/guides/index.json +2 -1
  39. package/guides/qa-builds.md +34 -8
  40. package/guides/sdk-reference.json +17 -5
  41. package/guides/tokens.md +22 -1
  42. package/package.json +2 -2
  43. package/templates/branded-lite/dev/index.ts +41 -0
  44. package/templates/branded-lite/tier5-src/_shared/iap-worker.ts +21 -44
  45. package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +77 -47
  46. package/templates/branded-lite/tier5-src/himi-branded-lite-programs/index.ts +24 -38
  47. package/templates/brandy/README.md +5 -3
  48. package/templates/yoga-studio/dev/index.ts +41 -0
  49. package/templates/yoga-studio/tier5-src/_shared/iap-worker.ts +21 -44
  50. package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +77 -47
  51. package/templates/yoga-studio/tier5-src/himi-branded-lite-programs/index.ts +24 -38
Binary file
@@ -59,6 +59,23 @@ The screen list is enforced at runtime: a call the screen did not declare is **d
59
59
  so it must name every capability the app uses. A background variant (`location_background`,
60
60
  `audio_record_background`) is declared instead of — and grants — its foreground capability.
61
61
 
62
+ ### Local database and tenant switches
63
+
64
+ On native hosts `local_database` is also scoped by the host's active tenant. On a multi-tenant host such as Himi Player, a worker is stamped with the tenant that was active when its boot started. The
65
+ stamp travels with each dispatch; if the host switches tenants while a dispatch is in flight,
66
+ the native handler refuses it at store or write-queue resolution instead of retargeting the
67
+ request to the new tenant.
68
+
69
+ The tenant's `dataSchema` is the allowlist for local entities and must be applied for the
70
+ tenant whose config supplied it. A missing schema is applied as `{}` and clears the previous
71
+ allowlist and encrypted backing configuration. Himi Player applies cached package config
72
+ before an imported-package fetch and reapplies the fetched schema after foreground/reload
73
+ refreshes only when the refresh still belongs to the current release and tenant; snapshot
74
+ packages and stale or overtaken refreshes are not overwritten.
75
+
76
+ The web host does not have this scoping yet: its IndexedDB `EntityStore` keys rows by
77
+ `entity:<name>` only, so on the web Player two apps that use the same entity name share rows.
78
+
62
79
  ## 3. Check
63
80
 
64
81
  ```bash
@@ -408,6 +408,8 @@ nicety. Don't animate everything: if every element moves, nothing reads as impor
408
408
  - **`accessibility_label` on every non-text control, icon, and image.** This is **distinct from `id`** —
409
409
  `id` targets a node for Argus/test automation; `accessibility_label` is what a screen reader announces.
410
410
  A screen full of `id`s and no `accessibility_label`s is testable but unusable with VoiceOver/TalkBack.
411
+ It may be bound to worker state with `$service:<service>.<path>`; the web renderer keeps the
412
+ resulting `aria-label` current as that state changes.
411
413
  - **Tap targets** stay comfortably large (don't shrink `button`/`icon` hit areas below ~44pt).
412
414
  - **Contrast** comes from the token roles — pair `textPrimary`/`textSecondary` with the right surface,
413
415
  and provide the `highContrast` scheme value for load-bearing colors.
@@ -269,8 +269,10 @@ and `himi site function` (not analyze, and not `himi functions`).
269
269
  **Selling digital items** (programs, products, plans the owner registered with `himi store iap`)?
270
270
  Any screen that shows such an item's price — a template screen or one you write — gets its App
271
271
  Store price and payment route from the shared `createIapCatalogPricing` in `@himalaya/iap`:
272
- read `himi docs iap-pricing` before writing the worker. Starting from a template does not cover
273
- your own screens; each screen opts in explicitly.
272
+ read `himi docs iap-pricing` before writing the worker. A screen that sells the item composes
273
+ `createIapCommerce` from `@himalaya/iap-checkout` and binds Get access with
274
+ `commerce.purchaseFields`, never StoreKit or the payment client directly. Starting from a template does
275
+ not cover your own screens; each screen opts in explicitly.
274
276
 
275
277
  After `himi init … --template branded-lite` (or a vertical overlay), **point the app at your
276
278
  Wix site** before expecting live catalog data:
@@ -292,8 +292,10 @@ and `himi site function` (not analyze, and not `himi functions`).
292
292
  **Selling digital items** (programs, products, plans the owner registered with `himi store iap`)?
293
293
  Any screen that shows such an item's price — a template screen or one you write — gets its App
294
294
  Store price and payment route from the shared `createIapCatalogPricing` in `@himalaya/iap`:
295
- read `himi docs iap-pricing` before writing the worker. Starting from a template does not cover
296
- your own screens; each screen opts in explicitly.
295
+ read `himi docs iap-pricing` before writing the worker. A screen that sells the item composes
296
+ `createIapCommerce` from `@himalaya/iap-checkout` and binds Get access with
297
+ `commerce.purchaseFields`, never StoreKit or the payment client directly. Starting from a template does
298
+ not cover your own screens; each screen opts in explicitly.
297
299
 
298
300
  After `himi init … --template branded-lite` (or a vertical overlay), **point the app at your
299
301
  Wix site** before expecting live catalog data:
@@ -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
- > **Not switched on yet.** Wix has not connected the service that maps your catalog to App Store
10
- > products. Until it does, every screen keeps its existing price and checkout. Apple purchasing is
11
- > not available in any case: this layer shows prices and blocks the wrong checkout, but never buys.
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; purchasing is not available yet |
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
- > **Buying is not switched on.** `@wix/himalaya/iap-checkout` is a shared App Store checkout,
38
- > `createAppleIapCheckout`, built on this layer. It answers `notEnabled` and makes no call until Wix
39
- > enables it, so keep App Store items blocked as described above. Never build your own purchase
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. Copy the pattern, not the course names.
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 { get as appInfo } from "@himalaya/app-info";
61
- import { createIapCatalogPricing, iapPriceFields, onlineProgramItem } from "@himalaya/iap";
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
- const pricing = createIapCatalogPricing({ onChange: () => { void service.publish(); } });
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) await pricing.load({ ...(await hostScope()), items: [item] });
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
- if (!pricing.enabled) return { ...state, priceText: course.commercePrice, buyDisabled: false, message };
95
- const price = iapPriceFields(pricing.view(item.reference), COPY);
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 both capabilities the worker calls. To show
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.** On Android, web and macOS, App Store routes come back `unavailable`. Regular
167
- routes are unaffected.
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 sell, whatever its vertical.
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, and Apple purchasing is not available yet.",
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.",
@@ -8,10 +8,12 @@ A **QA build** is a real, installable app you hand to QA — an enterprise/ad-ho
8
8
  distinct from the CI gates (`test:*`, `argus:*`), which only prove the code builds
9
9
  and passes flows; nothing installable comes out of those.
10
10
 
11
- > **QA only — not TestFlight.** `himi dist` returns an installable QA artifact and
12
- > install link. It never submits to App Store Connect or Google Play. In particular,
13
- > it is separate from Himi Player's Wix Mobile Release TestFlight route and its
14
- > output is not an MRE-approved upload artifact.
11
+ > **The default is QA; customer TestFlight is an explicit BYO path.** The ordinary
12
+ > `himi dist` request returns an installable QA artifact and install link. A customer
13
+ > iOS request may instead use `--signing byo --destination testflight --site <id>
14
+ > --version <x.y.z>`; the build validates the App Store IPA, and an authorized
15
+ > `himi dist status <jobId>` read uploads it to the customer's App Store Connect account.
16
+ > The customer path is not Himi Player's Wix Mobile Release route and does not use MRE.
15
17
 
16
18
  > **Customer request gate.** Do not dispatch `himi dist` merely because a draft preview is ready.
17
19
  > First confirm that the customer explicitly asked to download the app, identify the approved
@@ -32,8 +34,31 @@ This submits `destination: testflight`. The pool produces an IPA in the MRE-appr
32
34
  `wix-ios-app-builds` bucket; deployed serve then invokes MRE using its POCO service
33
35
  identity and persists the MRE request ID/status on the same Job Service record. It
34
36
  accepts only the `himi-player` app, `player` shell, iOS, and its own bundle identity.
35
- `himi dist` keeps `destination: link`, does not call MRE, and remains the QA path for
36
- Himi distribution apps.
37
+ The customer BYO path below also uses `destination: testflight`, but it uploads through
38
+ an authorized `himi dist status` read rather than MRE. The ordinary `himi dist` path
39
+ keeps `destination: link` and remains the QA path for Himi distribution apps.
40
+
41
+ ### Customer App Store / TestFlight handoff
42
+
43
+ For an owned app, the customer must authorize the selected site and provide the existing
44
+ customer distribution certificate plus an **App Store** provisioning profile (not the ad-hoc
45
+ profile the QA `--signing byo` path below selects) and an App Store Connect API key (key id,
46
+ issuer id and private key) in the site's customer signing binding. Without that key the
47
+ request fails with `customer_app_store_validation_key_missing`. The request is iOS-only:
48
+
49
+ ```bash
50
+ himi dist --app <app> --release <published-release-id> --site <owned-site-id> \
51
+ --platform ios --signing byo --destination testflight \
52
+ --version <marketing-version> --no-wait --target wix
53
+ himi dist status <jobId> --target wix
54
+ ```
55
+
56
+ The pool records `READY_FOR_UPLOAD` after validating the IPA. The status read proves site
57
+ ownership with the Wix credential, then claims and uploads the IPA once; repeat reads are
58
+ safe. Without that authorization, status reports the waiting state and does not upload.
59
+ The upload also requires customer App Store upload to be enabled on the Himi backend
60
+ (`HIMI_DIST_CUSTOMER_APP_STORE_UPLOAD=1`); where that switch is off, the job stays at `READY_FOR_UPLOAD` with a validated IPA that is
61
+ never uploaded.
37
62
 
38
63
  ### iOS bundle identities are channel-specific
39
64
 
@@ -147,8 +172,9 @@ The chosen policy is written to the job record (`signing`), so `himi dist status
147
172
  answer "whose certificate is on this binary" without re-deriving it from serve config that may
148
173
  have changed since.
149
174
 
150
- **`--signing byo`** uses an owned Wix site and its existing personal Apple distribution certificate
151
- and ad-hoc profile. Himi checks site ownership, the app's recorded native ID, profile
175
+ **`--signing byo`** (QA, `destination: link`) uses an owned Wix site and its existing personal
176
+ Apple distribution certificate and ad-hoc profile. The TestFlight handoff above uses the App Store
177
+ profile and ASC key instead; an ad-hoc setup does not satisfy it. Himi checks site ownership, the app's recorded native ID, profile
152
178
  type, certificate match, expiry, registered devices and production APNs entitlement before dispatch.
153
179
  It selects an existing compatible ad-hoc profile; the build does not create or revoke Apple assets.
154
180
  The runner fetches the certificate and profile using both its job token and a short-lived Buildkite
@@ -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 * NOT ENABLED and not production-ready. No app wires it, and no call reaches the service: it has\n * no public gateway route yet, so `route` must be supplied and there is 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 * Production use stays blocked: there is no public route or member authorization on the service,\n * no trusted app-binding source, and checkout stays disabled. 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 */",
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
  },
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1319.0",
3
+ "version": "0.1321.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": "60f160d9f74860a80bb7b1bedb582949eda4352aaf8909967561bac5"
74
+ "falconPackageHash": "3318113ec10ae018bbbe09e99bb3bef1207460516f9ca12d20e128ff"
75
75
  }
@@ -85162,6 +85162,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
85162
85162
  "actionId": "tier5.himiBrandedProgram.enroll"
85163
85163
  }
85164
85164
  }
85165
+ },
85166
+ "modifiers": {
85167
+ "hiddenPath": "$service:himiBrandedProgram.enrollHidden"
85165
85168
  }
85166
85169
  },
85167
85170
  {
@@ -85175,6 +85178,44 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
85175
85178
  "color": "textSecondary",
85176
85179
  "collapseWhenEmpty": true
85177
85180
  }
85181
+ },
85182
+ "modifiers": {
85183
+ "hiddenPath": "$service:himiBrandedProgram.enrollHidden"
85184
+ }
85185
+ },
85186
+ {
85187
+ "id": "himi_program.buy",
85188
+ "body": {
85189
+ "package": "himalaya",
85190
+ "type": "Button",
85191
+ "props": {
85192
+ "title": "$service:himiBrandedProgram.buyLabel",
85193
+ "style": "primary",
85194
+ "fullWidth": true,
85195
+ "disabledPath": "$service:himiBrandedProgram.buyDisabled",
85196
+ "action": {
85197
+ "actionId": "tier5.himiBrandedProgram.buy"
85198
+ }
85199
+ }
85200
+ },
85201
+ "modifiers": {
85202
+ "hiddenPath": "$service:himiBrandedProgram.buyHidden"
85203
+ }
85204
+ },
85205
+ {
85206
+ "id": "himi_program.buy_note",
85207
+ "body": {
85208
+ "package": "himalaya",
85209
+ "type": "Text",
85210
+ "props": {
85211
+ "text": "$service:himiBrandedProgram.buyNote",
85212
+ "style": "footnote",
85213
+ "color": "textSecondary",
85214
+ "collapseWhenEmpty": true
85215
+ }
85216
+ },
85217
+ "modifiers": {
85218
+ "hiddenPath": "$service:himiBrandedProgram.buyHidden"
85178
85219
  }
85179
85220
  },
85180
85221
  {
@@ -1,49 +1,11 @@
1
- // Worker-side glue for the shared IAP catalog layer (`@himalaya/iap` → createIapCatalogPricing),
2
- // shared by every screen that shows or sells an IAP-backed item. Mapping, Apple prices and payment
3
- // guards live in the SDK; this only supplies the host scope, the copy, and the hydration reset.
4
- import { get as appInfo } from "@himalaya/app-info";
5
- import type { IapCatalogScope, IapPriceCopy } from "@himalaya/iap";
1
+ // Worker-side glue for the shared IAP layer (`@himalaya/iap` catalog pricing and
2
+ // `@himalaya/iap-checkout` → createIapCommerce), shared by every screen that shows or sells an
3
+ // IAP-backed item. Mapping, Apple prices, payment guards, the host scope and the sale live in the
4
+ // SDK; this only supplies the copy and the hydration reset.
5
+ import type { IapPriceCopy } from "@himalaya/iap";
6
+ import type { IapPurchaseCopy } from "@himalaya/iap-checkout";
6
7
  import { copyParam } from "./himi-copy.js";
7
8
 
8
- export interface HostIapScope {
9
- scope: IapCatalogScope;
10
- platform: string;
11
- }
12
-
13
- let hostScope: Promise<HostIapScope> | undefined;
14
-
15
- /** Read once per worker, and only by an enabled screen: a disabled one makes no app-info call.
16
- * App identity and platform come from the host, never a screen param. The site is the host-owned
17
- * native preference business (the web partition uses an OAuth client ID); without one, the mapping source's
18
- * bound site applies. A failed read leaves identity empty, which fails every item closed — and is
19
- * not kept, so the next lookup asks the host again instead of staying invalid for good. */
20
- export function hostIapScope(): Promise<HostIapScope> {
21
- return hostScope ??= readHostScope().then((read) => {
22
- if (!hostScopeComplete(read)) hostScope = undefined;
23
- return read;
24
- });
25
- }
26
-
27
- /** An incomplete scope prices every item `invalidInput`, which the shared layer never offers to
28
- * retry — so a screen offers Retry itself while its own host read is what failed. */
29
- export function hostScopeComplete(read: HostIapScope): boolean {
30
- return !!read.scope.appId && !!read.scope.bundleId && !!read.platform;
31
- }
32
-
33
- async function readHostScope(): Promise<HostIapScope> {
34
- const identity = await appInfo().catch(() => undefined);
35
- const siteId = identity?.platform === "web" ? "" : hostSiteId();
36
- return {
37
- scope: { appId: identity?.appId ?? "", bundleId: identity?.bundleId ?? "", ...(siteId ? { siteId } : {}) },
38
- platform: identity?.platform ?? "",
39
- };
40
- }
41
-
42
- function hostSiteId(): string {
43
- const value = (globalThis as { himalaya?: { input?: Record<string, unknown> } }).himalaya?.input?.__himiPreferenceBusiness;
44
- return typeof value === "string" ? value.trim() : "";
45
- }
46
-
47
9
  /** Price labels from the screen's copy pack (`<prefix>.priceLoadingText`, …). */
48
10
  export function iapPriceCopy(prefix: string): IapPriceCopy {
49
11
  return {
@@ -54,6 +16,21 @@ export function iapPriceCopy(prefix: string): IapPriceCopy {
54
16
  };
55
17
  }
56
18
 
19
+ /** App Store purchase labels from the screen's copy pack (`<prefix>.buyCta`, …), for
20
+ * `iapPurchaseFields`. Customer copy only: no backend names, ids or diagnostics. */
21
+ export function iapPurchaseCopy(prefix: string): IapPurchaseCopy {
22
+ return {
23
+ buy: copyParam(`${prefix}.buyCta`, "Get access"),
24
+ processing: copyParam(`${prefix}.purchaseProcessingText`, "Processing your purchase…"),
25
+ awaitingApproval: copyParam(`${prefix}.purchaseAwaitingApprovalText`, "Waiting for approval"),
26
+ unavailable: copyParam(`${prefix}.purchaseTemporarilyUnavailableText`, "Purchasing is temporarily unavailable."),
27
+ signInRequired: copyParam(`${prefix}.purchaseSignInText`, "Sign in to buy."),
28
+ retry: copyParam(`${prefix}.purchaseRetryText`, "Something went wrong. Try again."),
29
+ priceChanged: copyParam(`${prefix}.purchasePriceChangedText`, "The price changed. Check it and try again."),
30
+ support: copyParam(`${prefix}.purchaseSupportText`, "We couldn't finish this purchase. Contact support."),
31
+ };
32
+ }
33
+
57
34
  /** What `notHydrated` does, for a runtime that ignores the option (a binary older than it, still
58
35
  * served new bundles): put the listed keys back to their declared values before any action runs,
59
36
  * so a persisted price or an enabled purchase action never paints as this run's. */