@wix/himalaya-cli 0.1352.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.
- package/package.json +2 -2
- package/templates/beauty-salon/tier5-src/himi-branded-lite-plans/index.ts +17 -0
- package/templates/beauty-salon/tier5-src/wix-spaces-plan-detail/index.ts +30 -4
- package/templates/branded-lite/tier5-src/himi-branded-lite-plans/index.ts +17 -0
- package/templates/branded-lite/tier5-src/himi-events-detail/index.ts +17 -3
- package/templates/branded-lite/tier5-src/wix-spaces-plan-detail/index.ts +30 -4
- package/templates/community-club/tier5-src/_shared/hosted-checkout.ts +42 -0
- package/templates/community-club/tier5-src/himi-events-detail/index.ts +17 -3
- package/templates/events-meetups/tier5-src/_shared/hosted-checkout.ts +42 -0
- package/templates/events-meetups/tier5-src/himi-events-detail/index.ts +17 -3
- package/templates/gym-fitness/tier5-src/himi-branded-lite-plans/index.ts +17 -0
- package/templates/gym-fitness/tier5-src/himi-events-detail/index.ts +17 -3
- package/templates/gym-fitness/tier5-src/wix-spaces-plan-detail/index.ts +30 -4
- package/templates/nonprofit-cause/tier5-src/_shared/hosted-checkout.ts +42 -0
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-plans/index.ts +17 -0
- package/templates/nonprofit-cause/tier5-src/himi-events-detail/index.ts +17 -3
- package/templates/nonprofit-cause/tier5-src/wix-spaces-plan-detail/index.ts +30 -4
- package/templates/podcast-creator/tier5-src/_shared/hosted-checkout.ts +42 -0
- package/templates/podcast-creator/tier5-src/himi-branded-lite-plans/index.ts +17 -0
- package/templates/podcast-creator/tier5-src/himi-events-detail/index.ts +17 -3
- package/templates/podcast-creator/tier5-src/wix-spaces-plan-detail/index.ts +30 -4
- package/templates/portfolio-creative/tier5-src/himi-events-detail/index.ts +17 -3
- package/templates/restaurant/tier5-src/_shared/hosted-checkout.ts +42 -0
- package/templates/restaurant/tier5-src/himi-events-detail/index.ts +17 -3
- package/templates/yoga-studio/tier5-src/himi-branded-lite-plans/index.ts +17 -0
- package/templates/yoga-studio/tier5-src/himi-events-detail/index.ts +17 -3
- package/templates/yoga-studio/tier5-src/wix-spaces-plan-detail/index.ts +30 -4
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Hosted checkout (#4089 B3). A Branded Lite hosted in a host shell (Spaces)
|
|
2
|
+
// pays as the member the host lent it: Headless `createRedirectSession` does not take a Member
|
|
3
|
+
// instance, so the HOST opens the site's checkout signed in as that member. The worker names only
|
|
4
|
+
// its ecom checkout; the host reads Wix's checkout URL, mints a one-time member session and opens
|
|
5
|
+
// the site's `createSessionCookie` → checkout. The session token never reaches this worker.
|
|
6
|
+
|
|
7
|
+
import { dispatch } from "@himalaya/dispatch";
|
|
8
|
+
import { hostSiteFromInput } from "./branded-lite-hosted.js";
|
|
9
|
+
|
|
10
|
+
/** The host verbs (registered by the host shell, never by the app itself). The first prepares the
|
|
11
|
+
* checkout and answers a one-time ticket; the second opens it. Two steps so the caller's charge guard
|
|
12
|
+
* is re-read after the host's network round trips and right before the checkout opens — the prepared
|
|
13
|
+
* URL (which carries the member's session token) stays in the host; only the ticket crosses here. */
|
|
14
|
+
export const HOSTED_CHECKOUT_ACTION = "himi.host.memberCheckout";
|
|
15
|
+
export const HOSTED_CHECKOUT_OPEN_ACTION = "himi.host.memberCheckout.open";
|
|
16
|
+
|
|
17
|
+
/** Whether this worker runs inside a host that opened a site for it. */
|
|
18
|
+
export function isHostedWorker(): boolean {
|
|
19
|
+
return hostSiteFromInput((globalThis as { himalaya?: { input?: unknown } }).himalaya?.input) !== null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type HostAnswer = { opened?: unknown; ticket?: unknown; payload?: { opened?: unknown; ticket?: unknown } } | null;
|
|
23
|
+
|
|
24
|
+
/** `payCashierOrder`'s `openCashier` for a hosted app: the host prepares [checkoutId]'s checkout as the
|
|
25
|
+
* lent member, the caller's guard is re-read, and only then does the host open it. Resolves whether it
|
|
26
|
+
* opened ("withdrawn" when the guard said no); a host without the verbs (or that refused) → false. */
|
|
27
|
+
export function hostedCheckoutOpener(checkoutId: string): (stillMayCharge: () => Promise<boolean>) => Promise<boolean | "withdrawn"> {
|
|
28
|
+
return async (stillMayCharge) => {
|
|
29
|
+
try {
|
|
30
|
+
const prepared = await dispatch(HOSTED_CHECKOUT_ACTION, { checkoutId }) as HostAnswer;
|
|
31
|
+
const ticket = prepared?.ticket ?? prepared?.payload?.ticket;
|
|
32
|
+
if (typeof ticket !== "string" || !ticket) return false;
|
|
33
|
+
// An unopened ticket simply expires in the host: nothing to cancel.
|
|
34
|
+
if (!(await stillMayCharge())) return "withdrawn";
|
|
35
|
+
const result = await dispatch(HOSTED_CHECKOUT_OPEN_ACTION, { ticket }) as HostAnswer;
|
|
36
|
+
const opened = result?.opened ?? result?.payload?.opened;
|
|
37
|
+
return opened === true || opened === "true";
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
28
|
-
|
|
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) }; }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Hosted checkout (#4089 B3). A Branded Lite hosted in a host shell (Spaces)
|
|
2
|
+
// pays as the member the host lent it: Headless `createRedirectSession` does not take a Member
|
|
3
|
+
// instance, so the HOST opens the site's checkout signed in as that member. The worker names only
|
|
4
|
+
// its ecom checkout; the host reads Wix's checkout URL, mints a one-time member session and opens
|
|
5
|
+
// the site's `createSessionCookie` → checkout. The session token never reaches this worker.
|
|
6
|
+
|
|
7
|
+
import { dispatch } from "@himalaya/dispatch";
|
|
8
|
+
import { hostSiteFromInput } from "./branded-lite-hosted.js";
|
|
9
|
+
|
|
10
|
+
/** The host verbs (registered by the host shell, never by the app itself). The first prepares the
|
|
11
|
+
* checkout and answers a one-time ticket; the second opens it. Two steps so the caller's charge guard
|
|
12
|
+
* is re-read after the host's network round trips and right before the checkout opens — the prepared
|
|
13
|
+
* URL (which carries the member's session token) stays in the host; only the ticket crosses here. */
|
|
14
|
+
export const HOSTED_CHECKOUT_ACTION = "himi.host.memberCheckout";
|
|
15
|
+
export const HOSTED_CHECKOUT_OPEN_ACTION = "himi.host.memberCheckout.open";
|
|
16
|
+
|
|
17
|
+
/** Whether this worker runs inside a host that opened a site for it. */
|
|
18
|
+
export function isHostedWorker(): boolean {
|
|
19
|
+
return hostSiteFromInput((globalThis as { himalaya?: { input?: unknown } }).himalaya?.input) !== null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type HostAnswer = { opened?: unknown; ticket?: unknown; payload?: { opened?: unknown; ticket?: unknown } } | null;
|
|
23
|
+
|
|
24
|
+
/** `payCashierOrder`'s `openCashier` for a hosted app: the host prepares [checkoutId]'s checkout as the
|
|
25
|
+
* lent member, the caller's guard is re-read, and only then does the host open it. Resolves whether it
|
|
26
|
+
* opened ("withdrawn" when the guard said no); a host without the verbs (or that refused) → false. */
|
|
27
|
+
export function hostedCheckoutOpener(checkoutId: string): (stillMayCharge: () => Promise<boolean>) => Promise<boolean | "withdrawn"> {
|
|
28
|
+
return async (stillMayCharge) => {
|
|
29
|
+
try {
|
|
30
|
+
const prepared = await dispatch(HOSTED_CHECKOUT_ACTION, { checkoutId }) as HostAnswer;
|
|
31
|
+
const ticket = prepared?.ticket ?? prepared?.payload?.ticket;
|
|
32
|
+
if (typeof ticket !== "string" || !ticket) return false;
|
|
33
|
+
// An unopened ticket simply expires in the host: nothing to cancel.
|
|
34
|
+
if (!(await stillMayCharge())) return "withdrawn";
|
|
35
|
+
const result = await dispatch(HOSTED_CHECKOUT_OPEN_ACTION, { ticket }) as HostAnswer;
|
|
36
|
+
const opened = result?.opened ?? result?.payload?.opened;
|
|
37
|
+
return opened === true || opened === "true";
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
28
|
-
|
|
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) }; }
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Hosted checkout (#4089 B3). A Branded Lite hosted in a host shell (Spaces)
|
|
2
|
+
// pays as the member the host lent it: Headless `createRedirectSession` does not take a Member
|
|
3
|
+
// instance, so the HOST opens the site's checkout signed in as that member. The worker names only
|
|
4
|
+
// its ecom checkout; the host reads Wix's checkout URL, mints a one-time member session and opens
|
|
5
|
+
// the site's `createSessionCookie` → checkout. The session token never reaches this worker.
|
|
6
|
+
|
|
7
|
+
import { dispatch } from "@himalaya/dispatch";
|
|
8
|
+
import { hostSiteFromInput } from "./branded-lite-hosted.js";
|
|
9
|
+
|
|
10
|
+
/** The host verbs (registered by the host shell, never by the app itself). The first prepares the
|
|
11
|
+
* checkout and answers a one-time ticket; the second opens it. Two steps so the caller's charge guard
|
|
12
|
+
* is re-read after the host's network round trips and right before the checkout opens — the prepared
|
|
13
|
+
* URL (which carries the member's session token) stays in the host; only the ticket crosses here. */
|
|
14
|
+
export const HOSTED_CHECKOUT_ACTION = "himi.host.memberCheckout";
|
|
15
|
+
export const HOSTED_CHECKOUT_OPEN_ACTION = "himi.host.memberCheckout.open";
|
|
16
|
+
|
|
17
|
+
/** Whether this worker runs inside a host that opened a site for it. */
|
|
18
|
+
export function isHostedWorker(): boolean {
|
|
19
|
+
return hostSiteFromInput((globalThis as { himalaya?: { input?: unknown } }).himalaya?.input) !== null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type HostAnswer = { opened?: unknown; ticket?: unknown; payload?: { opened?: unknown; ticket?: unknown } } | null;
|
|
23
|
+
|
|
24
|
+
/** `payCashierOrder`'s `openCashier` for a hosted app: the host prepares [checkoutId]'s checkout as the
|
|
25
|
+
* lent member, the caller's guard is re-read, and only then does the host open it. Resolves whether it
|
|
26
|
+
* opened ("withdrawn" when the guard said no); a host without the verbs (or that refused) → false. */
|
|
27
|
+
export function hostedCheckoutOpener(checkoutId: string): (stillMayCharge: () => Promise<boolean>) => Promise<boolean | "withdrawn"> {
|
|
28
|
+
return async (stillMayCharge) => {
|
|
29
|
+
try {
|
|
30
|
+
const prepared = await dispatch(HOSTED_CHECKOUT_ACTION, { checkoutId }) as HostAnswer;
|
|
31
|
+
const ticket = prepared?.ticket ?? prepared?.payload?.ticket;
|
|
32
|
+
if (typeof ticket !== "string" || !ticket) return false;
|
|
33
|
+
// An unopened ticket simply expires in the host: nothing to cancel.
|
|
34
|
+
if (!(await stillMayCharge())) return "withdrawn";
|
|
35
|
+
const result = await dispatch(HOSTED_CHECKOUT_OPEN_ACTION, { ticket }) as HostAnswer;
|
|
36
|
+
const opened = result?.opened ?? result?.payload?.opened;
|
|
37
|
+
return opened === true || opened === "true";
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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;
|