@wix/himalaya-cli 0.1351.0 → 0.1353.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 (29) hide show
  1. package/dist/cli.mjs +538 -71
  2. package/guides/himi-json-output.md +26 -1
  3. package/package.json +2 -2
  4. package/templates/beauty-salon/tier5-src/himi-branded-lite-plans/index.ts +17 -0
  5. package/templates/beauty-salon/tier5-src/wix-spaces-plan-detail/index.ts +30 -4
  6. package/templates/branded-lite/tier5-src/himi-branded-lite-plans/index.ts +17 -0
  7. package/templates/branded-lite/tier5-src/himi-events-detail/index.ts +17 -3
  8. package/templates/branded-lite/tier5-src/wix-spaces-plan-detail/index.ts +30 -4
  9. package/templates/community-club/tier5-src/_shared/hosted-checkout.ts +42 -0
  10. package/templates/community-club/tier5-src/himi-events-detail/index.ts +17 -3
  11. package/templates/events-meetups/tier5-src/_shared/hosted-checkout.ts +42 -0
  12. package/templates/events-meetups/tier5-src/himi-events-detail/index.ts +17 -3
  13. package/templates/gym-fitness/tier5-src/himi-branded-lite-plans/index.ts +17 -0
  14. package/templates/gym-fitness/tier5-src/himi-events-detail/index.ts +17 -3
  15. package/templates/gym-fitness/tier5-src/wix-spaces-plan-detail/index.ts +30 -4
  16. package/templates/nonprofit-cause/tier5-src/_shared/hosted-checkout.ts +42 -0
  17. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-plans/index.ts +17 -0
  18. package/templates/nonprofit-cause/tier5-src/himi-events-detail/index.ts +17 -3
  19. package/templates/nonprofit-cause/tier5-src/wix-spaces-plan-detail/index.ts +30 -4
  20. package/templates/podcast-creator/tier5-src/_shared/hosted-checkout.ts +42 -0
  21. package/templates/podcast-creator/tier5-src/himi-branded-lite-plans/index.ts +17 -0
  22. package/templates/podcast-creator/tier5-src/himi-events-detail/index.ts +17 -3
  23. package/templates/podcast-creator/tier5-src/wix-spaces-plan-detail/index.ts +30 -4
  24. package/templates/portfolio-creative/tier5-src/himi-events-detail/index.ts +17 -3
  25. package/templates/restaurant/tier5-src/_shared/hosted-checkout.ts +42 -0
  26. package/templates/restaurant/tier5-src/himi-events-detail/index.ts +17 -3
  27. package/templates/yoga-studio/tier5-src/himi-branded-lite-plans/index.ts +17 -0
  28. package/templates/yoga-studio/tier5-src/himi-events-detail/index.ts +17 -3
  29. package/templates/yoga-studio/tier5-src/wix-spaces-plan-detail/index.ts +30 -4
@@ -9,6 +9,7 @@ import { hostSessionFromGlobal, iapPriceFields, type IapCatalogItem } from "@him
9
9
  import { createIapCommerce, pricingPlanOrders } from "@himalaya/iap-checkout";
10
10
  import { iapPriceCopy, iapPurchaseCopy, resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
11
11
  import { formatCurrency } from "../_shared/engine-intl.js";
12
+ import { isHostedWorker } from "../_shared/hosted-checkout.js";
12
13
 
13
14
  /** Wix Pricing Plans appDefId — the vertical that owns a plan order, so the `appId` its
14
15
  * cashier order is paid under (mirrors AppDefId.PRICING_PLANS in Wix's RN apps). */
@@ -49,6 +50,8 @@ interface PlanRow {
49
50
  hasBadge: boolean;
50
51
  /** Purchasable URL when the plan data carries one; "" otherwise. */
51
52
  joinUrl: string;
53
+ /** No price (or a zero price): an online order activates it at once, with nothing to pay. */
54
+ free: boolean;
52
55
  joined: boolean;
53
56
  appleSupported: boolean;
54
57
  joinLabel: string;
@@ -117,6 +120,11 @@ function joinUrl(plan: WixPlan): string {
117
120
  return "";
118
121
  }
119
122
 
123
+ function isFreePlan(plan: WixPlan): boolean {
124
+ const raw = plan.pricing?.price?.value;
125
+ return raw == null || raw === "" || Number(raw) === 0;
126
+ }
127
+
120
128
  function planRow(plan: WixPlan): PlanRow {
121
129
  const name = (plan.name || copyParam("plans.planFallbackName", "Plan")).trim();
122
130
  const perks = (plan.perks?.values ?? [])
@@ -142,6 +150,7 @@ function planRow(plan: WixPlan): PlanRow {
142
150
  noBadge: !badge,
143
151
  hasBadge: !!badge,
144
152
  joinUrl: joinUrl(plan),
153
+ free: isFreePlan(plan),
145
154
  appleSupported: plan.pricing?.singlePaymentUnlimited === true,
146
155
  joined: false, joinLabel: copyParam("plans.joinCta", "Join"), joinDisabled: true, joinNote: "",
147
156
  };
@@ -215,6 +224,14 @@ async function regularJoinMessage(row: PlanRow, reference: { kind: string; id: s
215
224
  } catch {
216
225
  }
217
226
  }
227
+ // Hosted inside a host shell (Spaces) a plan cannot be paid in-app: classic Pricing Plans is not on
228
+ // ecom, so the host's member checkout has no checkout to open, and the redirect session needs a
229
+ // headless OAuth client a hosted app does not have. An online order created here would only be
230
+ // left behind as a DRAFT, so none is — the member joins on the site. A FREE plan has nothing to
231
+ // pay: its online order activates at once, so it still joins in-app.
232
+ if (isHostedWorker() && !row.free) {
233
+ return copyTemplate("plans.joinOnSiteMessage", "Memberships are purchased on the website — visit the site to join {name}.", { name: row.name });
234
+ }
218
235
  try {
219
236
  const created = await createOnlineOrder(row.id);
220
237
  const snapshotId = created.purchaseFlowId;
@@ -13,6 +13,7 @@ import {
13
13
  queryAvailableTickets,
14
14
  queryEvents,
15
15
  type WixAvailableTicket,
16
+ type WixEventFieldset,
16
17
  type WixEvent,
17
18
  type WixGuestRsvp,
18
19
  type WixGuestTicket,
@@ -34,6 +35,7 @@ import {
34
35
  type EventRow,
35
36
  } from "../_shared/himi-events";
36
37
  import { initials } from "../_shared/himi-text.js";
38
+ import { isHostedWorker } from "../_shared/hosted-checkout.js";
37
39
  import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
38
40
  import { openRichLink } from "../_shared/rich-content-link.js";
39
41
  import { richBody } from "../_shared/rich-body.js";
@@ -281,6 +283,10 @@ function calendarFallbackUrl(s: S, end: number): string {
281
283
  return `https://calendar.google.com/calendar/render?${parts.join("&")}`;
282
284
  }
283
285
 
286
+ /** The Events v3 fieldsets the detail reads (see `WixEventFieldset`). DETAILS carries mainImage and
287
+ * shortDescription: REGISTRATION fills eventType, which skips the query enrichment that used to supply them. */
288
+ export const EVENT_DETAIL_FIELDSETS: WixEventFieldset[] = ["DETAILS", "TEXTS", "REGISTRATION", "URLS"];
289
+
284
290
  /** Map the decoded registration/eventType onto the CTA variant. The decoded
285
291
  * `eventType` already folds registration.type / registration.initialType /
286
292
  * the legacy flat `type` (decodeEvent, #478). Unknown ⇒ RSVP (historical). */
@@ -458,8 +464,11 @@ const svc = defineService<S, {
458
464
  const id = s.id || inputId();
459
465
  if (!id) return derive({ ...s, status: "ready", errored: true });
460
466
  try {
461
- // TEXTS adds the Ricos `description` to the base fieldset (a bare read returns none).
462
- const res = await getEvent(id, { fields: ["TEXTS"] });
467
+ // TEXTS adds the Ricos `description` to the base fieldset (a bare read returns none);
468
+ // REGISTRATION adds `registration` (the CTA's type, and an external event's own URL);
469
+ // URLS adds `eventPageUrl` — the registration fallback and the share link. Without URLS a
470
+ // ticket flow that cannot complete in-app had no page to send the buyer to.
471
+ const res = await getEvent(id, { fields: EVENT_DETAIL_FIELDSETS });
463
472
  let e = ((res as unknown as { event?: WixEvent }).event ?? res) as WixEvent;
464
473
  e = await enrichRegistration(id, e);
465
474
  const attendees = await attendeeSummary(id);
@@ -566,7 +575,12 @@ const svc = defineService<S, {
566
575
  // Every failure degrades one rung at a time, never to a dead button:
567
576
  // native chain → hosted eventsCheckout (pinned to the SAME reservation
568
577
  // when we got one, else the slug) → the event page / registration URL.
569
- if (s.registrationKind === "tickets" && s.id) {
578
+ // Hosted inside a host shell (Spaces) neither in-app rung can finish: the classic
579
+ // `checkoutReservation` needs a buyer + guest form this screen does not collect, and the
580
+ // redirect session needs a headless OAuth client a hosted app does not have. Skip both —
581
+ // the native chain would also leave a ticket reservation held for nothing — and open the
582
+ // event's own page, where the member registers on the site.
583
+ if (s.registrationKind === "tickets" && s.id && !isHostedWorker()) {
570
584
  let reservationId = "";
571
585
  try {
572
586
  // Buy what the buyer PICKED. `load` seeds the pick with the cheapest
@@ -1,7 +1,13 @@
1
1
  // Tier-5 bundle: Wix Spaces clone — Pricing plan detail (ON-DEVICE, site-scoped).
2
- // himalaya.input.id carries the plan id; fetched via getPlan.
2
+ // himalaya.input.id carries the plan id.
3
+ //
4
+ // Read from the PUBLIC plans (GET /pricing-plans/v2/plans/public — what the plans list
5
+ // shows), not from getPlan: GET /pricing-plans/v2/plans/{id} is the management read, and a
6
+ // hosted site's member or visitor gets a 403 from it, which rendered "couldn't be loaded"
7
+ // for every plan the list had just shown. getPlan stays as the fallback for a plan that is
8
+ // not public, for a caller that may read it (an owner-scope host).
3
9
  import { withIapPresentation } from "../_shared/iap-presentation.js";
4
- import { getPlan, type WixPlan } from "@himalaya/wix-pricing-plans";
10
+ import { getPlan, listPublicPlans, type WixPlan } from "@himalaya/wix-pricing-plans";
5
11
  import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
6
12
 
7
13
  interface S extends PresenceFlags { status: "loading" | "ready"; name: string; price: string; description: string; perks: string }
@@ -12,6 +18,26 @@ function priceOf(p: WixPlan): string {
12
18
  return m.formattedAmount || [m.amount ?? m.value, m.currency].filter(Boolean).join(" ");
13
19
  }
14
20
 
21
+ /** The plan, or null when it is not public and the caller may not read it otherwise. */
22
+ async function readPlan(id: string): Promise<WixPlan | null> {
23
+ let publicRead = false;
24
+ try {
25
+ const listed = await listPublicPlans();
26
+ publicRead = true;
27
+ const hit = (listed?.plans ?? []).find((plan) => plan?.id === id);
28
+ if (hit) return hit;
29
+ } catch (e) { void e; }
30
+ try {
31
+ const res = await getPlan(id);
32
+ return ((res as unknown as { plan?: WixPlan }).plan) ?? (res as unknown as WixPlan);
33
+ } catch (e) {
34
+ // The public read worked, so the device is online: a refused or empty management
35
+ // read means this plan is not one the viewer can see — not-found, not offline.
36
+ if (publicRead) return null;
37
+ throw e;
38
+ }
39
+ }
40
+
15
41
  const defineService = withIapPresentation<S>({
16
42
  prefix: "plans", transientKeys: ["price"], contentActions: ["load"],
17
43
  items: state => detailInputId() ? [{ reference: { kind: "pricingPlan", id: detailInputId() }, commerceDisplayPrice: state.price }] : [],
@@ -24,8 +50,8 @@ const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesPlanDetail",
24
50
  const id = detailInputId();
25
51
  if (!id) return { ...s, status: "ready", ...presence("missing") };
26
52
  try {
27
- const res = await getPlan(id);
28
- const p = ((res as unknown as { plan?: WixPlan }).plan) ?? (res as unknown as WixPlan);
53
+ const p = await readPlan(id);
54
+ if (!p) return { ...s, status: "ready", ...presence("missing") };
29
55
  const perks = (p.perks?.values ?? []).map((v) => `• ${v}`).join("\n");
30
56
  return { status: "ready", name: p.name || "Plan", price: priceOf(p), description: p.description || "", perks, ...presence("found") };
31
57
  } catch (e) { return { ...s, status: "ready", ...presenceAfterError(e) }; }