@wix/himalaya-cli 0.1242.0 → 0.1244.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 (33) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/catalog-adapters/online-programs.d.ts +6 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/catalog-price-fields.d.ts +4 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/catalog-pricing-contract.d.ts +87 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/catalog-pricing-core.d.ts +10 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/catalog-pricing.d.ts +6 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/index.d.ts +4 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/mapping-source.d.ts +5 -0
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/tokens.d.ts +94 -0
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/checkout/src/index.d.ts +3 -0
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/checkout.mjs +3 -0
  11. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +40 -6
  12. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +192 -0
  13. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +3 -0
  14. package/dist/cli.mjs +365 -226
  15. package/dist/preview-assets.generated.bin +0 -0
  16. package/guides/getting-started.md +6 -0
  17. package/guides/himi-skill.md +6 -0
  18. package/guides/iap-pricing.md +162 -211
  19. package/guides/index.json +7 -6
  20. package/guides/sdk-reference.json +96 -0
  21. package/guides/tier-5-add-a-screen.md +9 -0
  22. package/guides/tokens.md +28 -0
  23. package/guides/wix-api-usage.md +11 -0
  24. package/guides/wix-site-to-himi-app.md +1 -1
  25. package/package.json +2 -2
  26. package/templates/branded-lite/dev/index.ts +110 -3
  27. package/templates/branded-lite/tier5-src/_shared/iap-worker.ts +65 -0
  28. package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +179 -52
  29. package/templates/branded-lite/tier5-src/himi-branded-lite-programs/index.ts +158 -38
  30. package/templates/yoga-studio/dev/index.ts +110 -3
  31. package/templates/yoga-studio/tier5-src/_shared/iap-worker.ts +65 -0
  32. package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +179 -52
  33. package/templates/yoga-studio/tier5-src/himi-branded-lite-programs/index.ts +158 -38
@@ -6,11 +6,19 @@
6
6
  // self-contained (name, cover, description snippet, cheap meta) — no detail
7
7
  // screen; enrolling stays a hosted-site flow.
8
8
  // Four-state (loading / error / rows / empty) like the sibling branded workers.
9
- import { defineService } from "../_shared/collection-service.js";
9
+ //
10
+ // App Store prices come from the shared IAP catalog layer (@himalaya/iap). Until a mapping backend
11
+ // is connected it is disabled, and this worker runs exactly its pre-pricing path.
12
+ import { defineService as register } from "@himalaya/state";
13
+ import { decorateCollection } from "../_shared/collection-service.js";
10
14
  import { queryPrograms, type WixProgram } from "@himalaya/wix-online-programs";
15
+ import { createIapCatalogPricing, iapPriceFields, onlineProgramItem, type IapPollDirective } from "@himalaya/iap";
11
16
  import { wixMediaUrl } from "@himalaya/wix-media";
12
17
  import { cleanTitle, initials } from "../_shared/himi-text.js";
13
18
  import { copyParam, stashCopy } from "../_shared/himi-copy.js";
19
+ import { hostIapScope, hostScopeComplete, iapPriceCopy, resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
20
+
21
+ const SERVICE = "himiBrandedPrograms";
14
22
 
15
23
  interface ProgramRow {
16
24
  id: string;
@@ -27,6 +35,15 @@ interface ProgramRow {
27
35
  noImage: boolean;
28
36
  /** Templated in-app program detail — rows tap through (no more dead wall). */
29
37
  detailScreenId: string;
38
+ /** This run's App Store price or its loading/unavailable label; "" when pricing is off. The
39
+ * collection card shows it in the caption ahead of `meta`. */
40
+ priceLine: string;
41
+ }
42
+
43
+ /** A row as loaded, before pricing — the half that may be restored from a cold-boot snapshot. */
44
+ interface ProgramContent extends Omit<ProgramRow, "priceLine"> {
45
+ /** The Wix ID, or "" when the row fell back to its title and so cannot be priced. */
46
+ wixId: string;
30
47
  }
31
48
 
32
49
  interface State {
@@ -40,6 +57,13 @@ interface State {
40
57
  hasRows: boolean;
41
58
  noRows: boolean;
42
59
  noEmpty: boolean;
60
+ hidePriceRetry: boolean;
61
+ /** Beside the list-level Retry, so it says what failed. */
62
+ priceRetryText: string;
63
+ programs?: ProgramContent[];
64
+ /** Derived from `rows` by the collection decorator. */
65
+ collection?: unknown;
66
+ _poll?: IapPollDirective;
43
67
  }
44
68
 
45
69
  /** v3 nests display fields under `description`/`settings`; older shapes are
@@ -86,7 +110,7 @@ function isDraft(p: WixProgram): boolean {
86
110
  return state === "DRAFT";
87
111
  }
88
112
 
89
- function programRow(p: WixProgram): ProgramRow {
113
+ function programFields(p: WixProgram): Omit<ProgramRow, "priceLine"> {
90
114
  const name = programTitle(p);
91
115
  const image = programImage(p);
92
116
  const id = p.id || name;
@@ -109,6 +133,14 @@ function programRow(p: WixProgram): ProgramRow {
109
133
  };
110
134
  }
111
135
 
136
+ function programRow(p: WixProgram): ProgramRow {
137
+ return { ...programFields(p), priceLine: "" };
138
+ }
139
+
140
+ function programContent(p: WixProgram): ProgramContent {
141
+ return { ...programFields(p), wixId: p.id ?? "" };
142
+ }
143
+
112
144
  function ready(rows: ProgramRow[]): State {
113
145
  return {
114
146
  status: "ready",
@@ -121,46 +153,134 @@ function ready(rows: ProgramRow[]): State {
121
153
  hasRows: rows.length > 0,
122
154
  noRows: rows.length === 0,
123
155
  noEmpty: rows.length > 0,
156
+ hidePriceRetry: true,
157
+ priceRetryText: "",
124
158
  };
125
159
  }
126
160
 
127
- const svc = defineService<State, {
128
- load: (state: State, arg?: unknown) => Promise<State>;
129
- }>("himiBrandedPrograms", {
130
- state: {
131
- status: "loading",
132
- rows: [],
133
- emptyText: "",
134
- isLoading: true,
135
- notLoading: false,
136
- isErrored: false,
137
- notErrored: true,
138
- hasRows: false,
139
- noRows: true,
161
+ // Bridge errors land here — a graceful error card with retry, never a dead
162
+ // screen (the branded four-state contract).
163
+ function errored(state: State): State {
164
+ return {
165
+ ...state,
166
+ status: "error",
167
+ isLoading: false,
168
+ notLoading: true,
169
+ isErrored: true,
170
+ notErrored: false,
140
171
  noEmpty: true,
141
- },
142
- actions: {
143
- load: async (state, arg) => {
144
- stashCopy(arg);
145
- try {
146
- const response = await queryPrograms({ cursorPaging: { limit: 50 } });
147
- return ready((response.programs ?? []).filter((p) => !isDraft(p)).map(programRow));
148
- } catch {
149
- // Bridge errors land here — a graceful error card with retry, never a
150
- // dead screen (the branded four-state contract).
151
- return {
152
- ...state,
153
- status: "error",
154
- isLoading: false,
155
- notLoading: true,
156
- isErrored: true,
157
- notErrored: false,
158
- noEmpty: true,
159
- };
160
- }
161
- },
162
- },
163
- });
172
+ };
173
+ }
174
+
175
+ async function fetchPrograms(): Promise<WixProgram[]> {
176
+ const response = await queryPrograms({ cursorPaging: { limit: 50 } });
177
+ return (response.programs ?? []).filter((p) => !isDraft(p));
178
+ }
179
+
180
+ async function load(state: State, arg?: unknown): Promise<State> {
181
+ stashCopy(arg);
182
+ try {
183
+ return ready((await fetchPrograms()).map(programRow));
184
+ } catch {
185
+ return errored(state);
186
+ }
187
+ }
188
+
189
+ // ── App Store pricing (enabled only when a mapping backend is connected) ─────
190
+ //
191
+ // One writer: `loadPriced` keeps its content here and every state change goes through the
192
+ // synchronous `pricingPublish`, which re-reads the latest content and prices. An async action
193
+ // that returned its own state could land after a newer publication and repaint stale prices.
194
+
195
+ const pricing = createIapCatalogPricing({ onChange: () => { void svc.pricingPublish(); } });
196
+ const PRICING_NOT_HYDRATED = ["rows", "collection", "hidePriceRetry"] as const;
197
+ let content: { status: State["status"]; programs: ProgramContent[] } = { status: "loading", programs: [] };
198
+ let scopeIncomplete = false;
199
+ let loadSeq = 0;
200
+
201
+ async function loadPriced(_state: State, arg?: unknown): Promise<undefined> {
202
+ stashCopy(arg);
203
+ const seq = ++loadSeq;
204
+ const programs = await fetchPrograms().catch(() => undefined);
205
+ if (seq !== loadSeq) return undefined;
206
+ content = programs ? { status: "ready", programs: programs.map(programContent) } : { ...content, status: "error" };
207
+ void svc.pricingPublish();
208
+ if (programs) void priceContent();
209
+ return undefined;
210
+ }
211
+
212
+ /** Prices the programs on screen now. Content never waits on this, and the host scope is re-read
213
+ * each time, so a failed app-info read recovers on the next refresh. */
214
+ async function priceContent(): Promise<void> {
215
+ // The Retry action is declared in both modes; with no backend it must not reach the host.
216
+ if (!pricing.enabled) return;
217
+ const host = await hostIapScope();
218
+ scopeIncomplete = !hostScopeComplete(host);
219
+ if (content.status !== "ready") return;
220
+ await pricing.load({ ...host, items: content.programs.flatMap((program) => onlineProgramItem({ id: program.wixId }) ?? []) });
221
+ }
222
+
223
+ function pricedState(state: State): State {
224
+ const copy = iapPriceCopy("programs");
225
+ const views = content.programs.map((program) => {
226
+ const item = onlineProgramItem({ id: program.wixId });
227
+ return item ? pricing.view(item.reference) : undefined;
228
+ });
229
+ const rows = content.programs.map(({ wixId: _wixId, ...row }, i) =>
230
+ ({ ...row, priceLine: views[i] ? iapPriceFields(views[i]!, copy).priceText : "" }));
231
+ const shown = content.status === "ready" ? ready(rows) : content.status === "error" ? errored({ ...state, rows }) : INITIAL_STATE;
232
+ return {
233
+ ...state, ...shown, programs: content.programs,
234
+ // Retry re-prices the programs on screen, which only a ready list has.
235
+ hidePriceRetry: content.status !== "ready" || !views.some((view) => view && (scopeIncomplete || view.retryable)),
236
+ priceRetryText: copyParam("programs.priceRetryText", "Some prices could not be loaded."),
237
+ };
238
+ }
239
+
240
+ const INITIAL_STATE: State = {
241
+ status: "loading",
242
+ rows: [],
243
+ emptyText: "",
244
+ isLoading: true,
245
+ notLoading: false,
246
+ isErrored: false,
247
+ notErrored: true,
248
+ hasRows: false,
249
+ noRows: true,
250
+ noEmpty: true,
251
+ hidePriceRetry: true,
252
+ priceRetryText: "",
253
+ };
254
+
255
+ type Actions = {
256
+ load: (state: State, arg?: unknown) => Promise<State | undefined>;
257
+ pricingPublish: (state: State) => State;
258
+ pricingStart: (state: State) => State;
259
+ pricingRefresh: (state: State) => Promise<undefined>;
260
+ };
261
+
262
+ const actions: Actions = {
263
+ load: pricing.enabled ? loadPriced : load,
264
+ pricingPublish: pricedState,
265
+ pricingStart: (state) => ({ ...pricedState(state), _poll: pricing.pollDirective(`${SERVICE}.pricingRefresh`) }),
266
+ pricingRefresh: async () => { await priceContent(); return undefined; },
267
+ };
268
+
269
+ // The collection decorator's own `defineService` drops `notHydrated`, so the decorated definition
270
+ // is registered directly: this run's prices must never be restored from a cold-boot snapshot.
271
+ const decorated = decorateCollection(SERVICE, { state: INITIAL_STATE, actions } as unknown as Parameters<typeof decorateCollection>[1]);
272
+ const svc = register<State, Actions>(SERVICE, (pricing.enabled
273
+ ? { ...decorated, notHydrated: [...PRICING_NOT_HYDRATED] }
274
+ : decorated) as unknown as Parameters<typeof register<State, Actions>>[1]);
275
+
276
+ if (pricing.enabled) {
277
+ const booted = svc.getState();
278
+ if ((booted.status === "ready" || booted.status === "error") && Array.isArray(booted.programs)) {
279
+ content = { status: booted.status, programs: booted.programs };
280
+ }
281
+ resetUnhydratedKeysOnOlderRuntime(svc, decorated.state as unknown as State, PRICING_NOT_HYDRATED);
282
+ void svc.pricingStart();
283
+ }
164
284
 
165
285
  // Kick the first load at module scope, like every other list bundle
166
286
  // (himi-store-shop, himi-booking-services, himi-events-list, himi-blog-feed …).
@@ -11598,6 +11598,65 @@ const SCREENS = runtimeSetting({
11598
11598
  "hiddenPath": "$service:himiBrandedPrograms.notErrored"
11599
11599
  }
11600
11600
  },
11601
+ {
11602
+ "id": "himi_programs.price_retry_card",
11603
+ "body": {
11604
+ "package": "himalaya",
11605
+ "type": "VStack",
11606
+ "props": {
11607
+ "spacing": 8,
11608
+ "alignment": "leading",
11609
+ "children": [
11610
+ {
11611
+ "id": "himi_programs.price_retry_text",
11612
+ "body": {
11613
+ "package": "himalaya",
11614
+ "type": "Text",
11615
+ "props": {
11616
+ "text": "$service:himiBrandedPrograms.priceRetryText",
11617
+ "style": "footnote",
11618
+ "color": "textSecondary",
11619
+ "collapseWhenEmpty": true
11620
+ }
11621
+ }
11622
+ },
11623
+ {
11624
+ "id": "himi_programs.price_retry",
11625
+ "body": {
11626
+ "package": "himalaya",
11627
+ "type": "Button",
11628
+ "props": {
11629
+ "title": "Try again",
11630
+ "style": "secondary",
11631
+ "action": {
11632
+ "actionId": "tier5.himiBrandedPrograms.pricingRefresh"
11633
+ }
11634
+ }
11635
+ }
11636
+ }
11637
+ ]
11638
+ }
11639
+ },
11640
+ "modifiers": {
11641
+ "background": {
11642
+ "token": "surfaceElevated"
11643
+ },
11644
+ "border": {
11645
+ "width": 1,
11646
+ "color": {
11647
+ "token": "border"
11648
+ }
11649
+ },
11650
+ "cornerRadius": 16,
11651
+ "padding": {
11652
+ "leading": 16,
11653
+ "trailing": 16,
11654
+ "top": 16,
11655
+ "bottom": 16
11656
+ },
11657
+ "hiddenPath": "$service:himiBrandedPrograms.hidePriceRetry"
11658
+ }
11659
+ },
11601
11660
  {
11602
11661
  "id": "himi_programs.rows.collection.toolbar",
11603
11662
  "body": {
@@ -13953,7 +14012,9 @@ const SCREENS = runtimeSetting({
13953
14012
  "integrity": "",
13954
14013
  "contentType": "application/javascript+ts-bundle",
13955
14014
  "declaredCapabilities": [
13956
- "wix_api"
14015
+ "wix_api",
14016
+ "iap",
14017
+ "app_info"
13957
14018
  ],
13958
14019
  "minBinaryVersion": "0.1.0",
13959
14020
  "bundleVersion": "0.0.1"
@@ -25931,7 +25992,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
25931
25992
  }
25932
25993
  },
25933
25994
  {
25934
- "id": "",
25995
+ "id": "himi_program.price",
25935
25996
  "body": {
25936
25997
  "package": "himalaya",
25937
25998
  "type": "Text",
@@ -25943,6 +26004,36 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
25943
26004
  }
25944
26005
  }
25945
26006
  },
26007
+ {
26008
+ "id": "himi_program.price_note",
26009
+ "body": {
26010
+ "package": "himalaya",
26011
+ "type": "Text",
26012
+ "props": {
26013
+ "text": "$service:himiBrandedProgram.priceNote",
26014
+ "style": "caption",
26015
+ "color": "textSecondary",
26016
+ "collapseWhenEmpty": true
26017
+ }
26018
+ }
26019
+ },
26020
+ {
26021
+ "id": "himi_program.price_retry",
26022
+ "body": {
26023
+ "package": "himalaya",
26024
+ "type": "Button",
26025
+ "props": {
26026
+ "title": "Try again",
26027
+ "style": "secondary",
26028
+ "action": {
26029
+ "actionId": "tier5.himiBrandedProgram.pricingRefresh"
26030
+ }
26031
+ }
26032
+ },
26033
+ "modifiers": {
26034
+ "hiddenPath": "$service:himiBrandedProgram.hidePriceRetry"
26035
+ }
26036
+ },
25946
26037
  {
25947
26038
  "id": "",
25948
26039
  "body": {
@@ -25997,12 +26088,26 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
25997
26088
  "title": "Enrol",
25998
26089
  "style": "primary",
25999
26090
  "fullWidth": true,
26091
+ "disabledPath": "$service:himiBrandedProgram.enrollDisabled",
26000
26092
  "action": {
26001
26093
  "actionId": "tier5.himiBrandedProgram.enroll"
26002
26094
  }
26003
26095
  }
26004
26096
  }
26005
26097
  },
26098
+ {
26099
+ "id": "himi_program.enrol_note",
26100
+ "body": {
26101
+ "package": "himalaya",
26102
+ "type": "Text",
26103
+ "props": {
26104
+ "text": "$service:himiBrandedProgram.enrollNote",
26105
+ "style": "footnote",
26106
+ "color": "textSecondary",
26107
+ "collapseWhenEmpty": true
26108
+ }
26109
+ }
26110
+ },
26006
26111
  {
26007
26112
  "id": "himi_program.action_message",
26008
26113
  "body": {
@@ -26142,7 +26247,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
26142
26247
  "wix_api",
26143
26248
  "wix_cashier",
26144
26249
  "open_url",
26145
- "wix_platform"
26250
+ "wix_platform",
26251
+ "iap",
26252
+ "app_info"
26146
26253
  ],
26147
26254
  "minBinaryVersion": "0.1.0",
26148
26255
  "bundleVersion": "0.0.1"
@@ -0,0 +1,65 @@
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";
6
+ import { copyParam } from "./himi-copy.js";
7
+
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
+ /** Price labels from the screen's copy pack (`<prefix>.priceLoadingText`, …). */
48
+ export function iapPriceCopy(prefix: string): IapPriceCopy {
49
+ return {
50
+ loading: copyParam(`${prefix}.priceLoadingText`, "Loading price…"),
51
+ unavailable: copyParam(`${prefix}.priceUnavailableText`, "Price unavailable"),
52
+ oneTimePurchase: copyParam(`${prefix}.oneTimePurchaseLabel`, "One-time purchase"),
53
+ purchaseUnavailable: copyParam(`${prefix}.purchaseUnavailableText`, "Purchasing is not available yet."),
54
+ };
55
+ }
56
+
57
+ /** What `notHydrated` does, for a runtime that ignores the option (a binary older than it, still
58
+ * served new bundles): put the listed keys back to their declared values before any action runs,
59
+ * so a persisted price or an enabled purchase action never paints as this run's. */
60
+ export function resetUnhydratedKeysOnOlderRuntime<S extends object>(svc: { getState(): S }, declared: S, keys: readonly (keyof S & string)[]): void {
61
+ if ((globalThis as { himalaya?: { hydrationOptOut?: unknown } }).himalaya?.hydrationOptOut === true) return;
62
+ const booted = svc.getState();
63
+ if (!booted || typeof booted !== "object") return;
64
+ for (const key of keys) booted[key] = declared[key];
65
+ }