@wix/himalaya-cli 0.1345.0 → 0.1348.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 (23) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/presigned-upload/src/index.d.ts +5 -12
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/checkout/src/index.d.ts +7 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/checkout.mjs +7 -2
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +7 -2
  5. package/dist/cli.mjs +102 -47
  6. package/dist/preview-assets.generated.bin +0 -0
  7. package/guides/sdk-reference.json +1 -1
  8. package/package.json +2 -2
  9. package/templates/beauty-salon/tier5-src/_shared/hosted-checkout.ts +42 -0
  10. package/templates/beauty-salon/tier5-src/himi-booking-time/index.ts +9 -3
  11. package/templates/beauty-salon/tier5-src/himi-branded-lite-cart/index.ts +11 -3
  12. package/templates/branded-lite/tier5-src/_shared/hosted-checkout.ts +42 -0
  13. package/templates/branded-lite/tier5-src/himi-booking-time/index.ts +9 -3
  14. package/templates/branded-lite/tier5-src/himi-branded-lite-cart/index.ts +11 -3
  15. package/templates/gym-fitness/tier5-src/_shared/hosted-checkout.ts +42 -0
  16. package/templates/gym-fitness/tier5-src/himi-booking-time/index.ts +9 -3
  17. package/templates/gym-fitness/tier5-src/himi-branded-lite-cart/index.ts +11 -3
  18. package/templates/portfolio-creative/tier5-src/_shared/hosted-checkout.ts +42 -0
  19. package/templates/portfolio-creative/tier5-src/himi-branded-lite-cart/index.ts +11 -3
  20. package/templates/retail-store/tier5-src/_shared/hosted-checkout.ts +42 -0
  21. package/templates/retail-store/tier5-src/himi-branded-lite-cart/index.ts +11 -3
  22. package/templates/yoga-studio/tier5-src/_shared/hosted-checkout.ts +42 -0
  23. package/templates/yoga-studio/tier5-src/himi-booking-time/index.ts +9 -3
Binary file
@@ -3543,7 +3543,7 @@
3543
3543
  {
3544
3544
  "name": "putFile",
3545
3545
  "kind": "function",
3546
- "jsdoc": "/**\n * Upload a stored file's bytes to a pre-signed URL, read straight from the\n * `@himalaya/files` substrate — the bytes never cross the worker bridge, so this is\n * the path for photos and voice notes.\n *\n * DECLARE \"files\" TOO. This verb reads the app's document substrate, so a bundle that\n * uses it is doing file access and its declared list should say so — the privacy\n * manifest and the generated native config are built from that list.\n *\n * Be aware of what is and is not enforced. The runtime gate keys on the capability id in\n * the action name, so `capability.presigned_upload.putFile` is admitted by declaring\n * `presigned_upload` alone: today a bundle can reach a substrate path without declaring\n * `files`. That is a gap in the gate, not a licence — and it is deliberately NOT closed\n * by adding `presigned_upload → files` to CAPABILITY_IMPLIES, because that table grants\n * supersets, and granting the WHOLE files capability (write, remove, list) to anything\n * that can upload would be a wider hole than the one it patched. Closing it properly\n * needs a requires-relation the gate does not have yet.\n */",
3546
+ "jsdoc": "/**\n * Upload a stored file's bytes to a pre-signed URL, read straight from the\n * `@himalaya/files` substrate — the bytes never cross the worker bridge, so this is\n * the path for photos and voice notes.\n *\n * DECLARE \"files\" TOO — it is REQUIRED. This verb reads the app's document substrate, so\n * the worker gate on every runtime (iOS, macOS, Android, web) refuses\n * `capability.presigned_upload.putFile` to a bundle that does not also declare `files`.\n * Deliberately a requirement and NOT a `presigned_upload → files` implication:\n * declaring `presigned_upload` grants none of the files surface (write, remove, list).\n */",
3547
3547
  "decl": "export async function putFile(\n url: string,\n ref: FileHandle | string,\n opts?: UploadOptions,\n): Promise<UploadResult>;"
3548
3548
  },
3549
3549
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1345.0",
3
+ "version": "0.1348.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": "2527f3bb9a9636577600b4f087d1a8bbd2c243ad80d31b17d53a6751"
74
+ "falconPackageHash": "ff7251a5688611526e72aaae9882c46d2f4f3667bbd4f0fa94b5343f"
75
75
  }
@@ -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 { bookSlot, getService, queryServiceAvailability, type WixBookingSlotFull
9
9
  import { createCheckout, createOrder } from "@himalaya/wix-ecom";
10
10
  import { createRedirectSession, type WixSlot } from "@himalaya/wix-redirects";
11
11
  import { payCashierOrder } from "@himalaya/checkout";
12
+ import { hostedCheckoutOpener, isHostedWorker } from "../_shared/hosted-checkout.js";
12
13
  import { session as wixPlatformSession } from "@himalaya/wix-platform";
13
14
  import { displayName, getMyData, type WixMyData } from "@himalaya/wix-users";
14
15
  import { formatServicePrice, serviceIsChargeable } from "../_shared/wix-service-price.js";
@@ -662,9 +663,12 @@ const svc = defineService<S, {
662
663
  // 3. Create the Wix-managed order — mints the cashier snapshot when a
663
664
  // balance is due. No paymentGatewayOrderId ⇒ nothing to charge
664
665
  // (covered / zero total) ⇒ the booking is already confirmed.
665
- const orderRes = await createOrder(checkoutId);
666
- const snapshotId = orderRes.paymentGatewayOrderId;
667
- if (!snapshotId) {
666
+ // Hosted (Spaces): the site's own checkout page creates the order (as
667
+ // RN does) — creating it here first fails on a site with no payment method set up.
668
+ const hosted = isHostedWorker();
669
+ const orderRes = hosted ? { paymentGatewayOrderId: "" } : await createOrder(checkoutId);
670
+ const snapshotId = orderRes.paymentGatewayOrderId ?? "";
671
+ if (!snapshotId && !hosted) {
668
672
  return derive({ ...pending, bookingInFlight: false, successMessage: copyParam("time.successMessage", "Booked. Your confirmation is ready in My Bookings."), message: "" });
669
673
  }
670
674
  // 4. Collect payment IN-APP — wallet when this app has one wired, otherwise
@@ -676,6 +680,8 @@ const svc = defineService<S, {
676
680
  appId: WIX_BOOKINGS_APP_ID,
677
681
  snapshotId,
678
682
  msid: await resolveMsid(),
683
+ // Hosted (Spaces): the host opens this booking's checkout as the lent member.
684
+ ...(hosted ? { openCashier: hostedCheckoutOpener(checkoutId) } : {}),
679
685
  cashierUrl: async () => {
680
686
  const redirect = await createRedirectSession({
681
687
  bookingsCheckout: { slotAvailability: { slot: slot as unknown as WixSlot } },
@@ -6,6 +6,7 @@ import { createCheckoutFromCurrentCart, createOrder, getCurrentCart, removeLineI
6
6
  import { createRedirectSession } from "@himalaya/wix-redirects";
7
7
  import { session as wixPlatformSession } from "@himalaya/wix-platform";
8
8
  import { payCashierOrder } from "@himalaya/checkout";
9
+ import { hostedCheckoutOpener, isHostedWorker } from "../_shared/hosted-checkout.js";
9
10
 
10
11
  /** Wix Stores appDefId — the vertical that owns a cart/ecom order, and therefore the
11
12
  * `appId` its cashier order is paid under. (Same constant Wix's own tooling uses.) */
@@ -317,9 +318,13 @@ const svc = defineService<State, {
317
318
  if (!checkout.checkoutId) return derive({ ...state, checkoutHint: copyParam("cart.checkoutIdMissingMessage", "Wix did not return a checkout id.") });
318
319
  const checkoutId = checkout.checkoutId;
319
320
 
320
- const order = await createOrder(checkoutId);
321
- const snapshotId = order.paymentGatewayOrderId;
322
- if (!snapshotId) {
321
+ // Hosted (Spaces): the site's own checkout page creates the order (RN's
322
+ // route) — creating it here first fails on a site with no payment method set up, before the
323
+ // member ever reaches the checkout. The host opens the checkout as the lent member.
324
+ const hosted = isHostedWorker();
325
+ const order = hosted ? { paymentGatewayOrderId: "" } : await createOrder(checkoutId);
326
+ const snapshotId = order.paymentGatewayOrderId ?? "";
327
+ if (!snapshotId && !hosted) {
323
328
  return derive({ ...state, ...cartConsumedByOrder() });
324
329
  }
325
330
 
@@ -335,6 +340,9 @@ const svc = defineService<State, {
335
340
  return !!reference && cartPricing.pricing.continueCheckoutGuard({ kind: "storesProduct", id: reference.catalogItemId }).allowed;
336
341
  });
337
342
  },
343
+ // Hosted (Spaces): the host opens the checkout as the lent member — Headless redirect
344
+ // sessions do not take a Member instance.
345
+ ...(hosted ? { openCashier: hostedCheckoutOpener(checkoutId) } : {}),
338
346
  cashierUrl: async () => {
339
347
  const redirect = await createRedirectSession({
340
348
  ecomCheckout: { checkoutId },
@@ -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 { bookSlot, getService, queryServiceAvailability, type WixBookingSlotFull
9
9
  import { createCheckout, createOrder } from "@himalaya/wix-ecom";
10
10
  import { createRedirectSession, type WixSlot } from "@himalaya/wix-redirects";
11
11
  import { payCashierOrder } from "@himalaya/checkout";
12
+ import { hostedCheckoutOpener, isHostedWorker } from "../_shared/hosted-checkout.js";
12
13
  import { session as wixPlatformSession } from "@himalaya/wix-platform";
13
14
  import { displayName, getMyData, type WixMyData } from "@himalaya/wix-users";
14
15
  import { formatServicePrice, serviceIsChargeable } from "../_shared/wix-service-price.js";
@@ -662,9 +663,12 @@ const svc = defineService<S, {
662
663
  // 3. Create the Wix-managed order — mints the cashier snapshot when a
663
664
  // balance is due. No paymentGatewayOrderId ⇒ nothing to charge
664
665
  // (covered / zero total) ⇒ the booking is already confirmed.
665
- const orderRes = await createOrder(checkoutId);
666
- const snapshotId = orderRes.paymentGatewayOrderId;
667
- if (!snapshotId) {
666
+ // Hosted (Spaces): the site's own checkout page creates the order (as
667
+ // RN does) — creating it here first fails on a site with no payment method set up.
668
+ const hosted = isHostedWorker();
669
+ const orderRes = hosted ? { paymentGatewayOrderId: "" } : await createOrder(checkoutId);
670
+ const snapshotId = orderRes.paymentGatewayOrderId ?? "";
671
+ if (!snapshotId && !hosted) {
668
672
  return derive({ ...pending, bookingInFlight: false, successMessage: copyParam("time.successMessage", "Booked. Your confirmation is ready in My Bookings."), message: "" });
669
673
  }
670
674
  // 4. Collect payment IN-APP — wallet when this app has one wired, otherwise
@@ -676,6 +680,8 @@ const svc = defineService<S, {
676
680
  appId: WIX_BOOKINGS_APP_ID,
677
681
  snapshotId,
678
682
  msid: await resolveMsid(),
683
+ // Hosted (Spaces): the host opens this booking's checkout as the lent member.
684
+ ...(hosted ? { openCashier: hostedCheckoutOpener(checkoutId) } : {}),
679
685
  cashierUrl: async () => {
680
686
  const redirect = await createRedirectSession({
681
687
  bookingsCheckout: { slotAvailability: { slot: slot as unknown as WixSlot } },
@@ -6,6 +6,7 @@ import { createCheckoutFromCurrentCart, createOrder, getCurrentCart, removeLineI
6
6
  import { createRedirectSession } from "@himalaya/wix-redirects";
7
7
  import { session as wixPlatformSession } from "@himalaya/wix-platform";
8
8
  import { payCashierOrder } from "@himalaya/checkout";
9
+ import { hostedCheckoutOpener, isHostedWorker } from "../_shared/hosted-checkout.js";
9
10
 
10
11
  /** Wix Stores appDefId — the vertical that owns a cart/ecom order, and therefore the
11
12
  * `appId` its cashier order is paid under. (Same constant Wix's own tooling uses.) */
@@ -317,9 +318,13 @@ const svc = defineService<State, {
317
318
  if (!checkout.checkoutId) return derive({ ...state, checkoutHint: copyParam("cart.checkoutIdMissingMessage", "Wix did not return a checkout id.") });
318
319
  const checkoutId = checkout.checkoutId;
319
320
 
320
- const order = await createOrder(checkoutId);
321
- const snapshotId = order.paymentGatewayOrderId;
322
- if (!snapshotId) {
321
+ // Hosted (Spaces): the site's own checkout page creates the order (RN's
322
+ // route) — creating it here first fails on a site with no payment method set up, before the
323
+ // member ever reaches the checkout. The host opens the checkout as the lent member.
324
+ const hosted = isHostedWorker();
325
+ const order = hosted ? { paymentGatewayOrderId: "" } : await createOrder(checkoutId);
326
+ const snapshotId = order.paymentGatewayOrderId ?? "";
327
+ if (!snapshotId && !hosted) {
323
328
  return derive({ ...state, ...cartConsumedByOrder() });
324
329
  }
325
330
 
@@ -335,6 +340,9 @@ const svc = defineService<State, {
335
340
  return !!reference && cartPricing.pricing.continueCheckoutGuard({ kind: "storesProduct", id: reference.catalogItemId }).allowed;
336
341
  });
337
342
  },
343
+ // Hosted (Spaces): the host opens the checkout as the lent member — Headless redirect
344
+ // sessions do not take a Member instance.
345
+ ...(hosted ? { openCashier: hostedCheckoutOpener(checkoutId) } : {}),
338
346
  cashierUrl: async () => {
339
347
  const redirect = await createRedirectSession({
340
348
  ecomCheckout: { checkoutId },
@@ -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 { bookSlot, getService, queryServiceAvailability, type WixBookingSlotFull
9
9
  import { createCheckout, createOrder } from "@himalaya/wix-ecom";
10
10
  import { createRedirectSession, type WixSlot } from "@himalaya/wix-redirects";
11
11
  import { payCashierOrder } from "@himalaya/checkout";
12
+ import { hostedCheckoutOpener, isHostedWorker } from "../_shared/hosted-checkout.js";
12
13
  import { session as wixPlatformSession } from "@himalaya/wix-platform";
13
14
  import { displayName, getMyData, type WixMyData } from "@himalaya/wix-users";
14
15
  import { formatServicePrice, serviceIsChargeable } from "../_shared/wix-service-price.js";
@@ -662,9 +663,12 @@ const svc = defineService<S, {
662
663
  // 3. Create the Wix-managed order — mints the cashier snapshot when a
663
664
  // balance is due. No paymentGatewayOrderId ⇒ nothing to charge
664
665
  // (covered / zero total) ⇒ the booking is already confirmed.
665
- const orderRes = await createOrder(checkoutId);
666
- const snapshotId = orderRes.paymentGatewayOrderId;
667
- if (!snapshotId) {
666
+ // Hosted (Spaces): the site's own checkout page creates the order (as
667
+ // RN does) — creating it here first fails on a site with no payment method set up.
668
+ const hosted = isHostedWorker();
669
+ const orderRes = hosted ? { paymentGatewayOrderId: "" } : await createOrder(checkoutId);
670
+ const snapshotId = orderRes.paymentGatewayOrderId ?? "";
671
+ if (!snapshotId && !hosted) {
668
672
  return derive({ ...pending, bookingInFlight: false, successMessage: copyParam("time.successMessage", "Booked. Your confirmation is ready in My Bookings."), message: "" });
669
673
  }
670
674
  // 4. Collect payment IN-APP — wallet when this app has one wired, otherwise
@@ -676,6 +680,8 @@ const svc = defineService<S, {
676
680
  appId: WIX_BOOKINGS_APP_ID,
677
681
  snapshotId,
678
682
  msid: await resolveMsid(),
683
+ // Hosted (Spaces): the host opens this booking's checkout as the lent member.
684
+ ...(hosted ? { openCashier: hostedCheckoutOpener(checkoutId) } : {}),
679
685
  cashierUrl: async () => {
680
686
  const redirect = await createRedirectSession({
681
687
  bookingsCheckout: { slotAvailability: { slot: slot as unknown as WixSlot } },
@@ -6,6 +6,7 @@ import { createCheckoutFromCurrentCart, createOrder, getCurrentCart, removeLineI
6
6
  import { createRedirectSession } from "@himalaya/wix-redirects";
7
7
  import { session as wixPlatformSession } from "@himalaya/wix-platform";
8
8
  import { payCashierOrder } from "@himalaya/checkout";
9
+ import { hostedCheckoutOpener, isHostedWorker } from "../_shared/hosted-checkout.js";
9
10
 
10
11
  /** Wix Stores appDefId — the vertical that owns a cart/ecom order, and therefore the
11
12
  * `appId` its cashier order is paid under. (Same constant Wix's own tooling uses.) */
@@ -317,9 +318,13 @@ const svc = defineService<State, {
317
318
  if (!checkout.checkoutId) return derive({ ...state, checkoutHint: copyParam("cart.checkoutIdMissingMessage", "Wix did not return a checkout id.") });
318
319
  const checkoutId = checkout.checkoutId;
319
320
 
320
- const order = await createOrder(checkoutId);
321
- const snapshotId = order.paymentGatewayOrderId;
322
- if (!snapshotId) {
321
+ // Hosted (Spaces): the site's own checkout page creates the order (RN's
322
+ // route) — creating it here first fails on a site with no payment method set up, before the
323
+ // member ever reaches the checkout. The host opens the checkout as the lent member.
324
+ const hosted = isHostedWorker();
325
+ const order = hosted ? { paymentGatewayOrderId: "" } : await createOrder(checkoutId);
326
+ const snapshotId = order.paymentGatewayOrderId ?? "";
327
+ if (!snapshotId && !hosted) {
323
328
  return derive({ ...state, ...cartConsumedByOrder() });
324
329
  }
325
330
 
@@ -335,6 +340,9 @@ const svc = defineService<State, {
335
340
  return !!reference && cartPricing.pricing.continueCheckoutGuard({ kind: "storesProduct", id: reference.catalogItemId }).allowed;
336
341
  });
337
342
  },
343
+ // Hosted (Spaces): the host opens the checkout as the lent member — Headless redirect
344
+ // sessions do not take a Member instance.
345
+ ...(hosted ? { openCashier: hostedCheckoutOpener(checkoutId) } : {}),
338
346
  cashierUrl: async () => {
339
347
  const redirect = await createRedirectSession({
340
348
  ecomCheckout: { checkoutId },
@@ -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
+ }
@@ -6,6 +6,7 @@ import { createCheckoutFromCurrentCart, createOrder, getCurrentCart, removeLineI
6
6
  import { createRedirectSession } from "@himalaya/wix-redirects";
7
7
  import { session as wixPlatformSession } from "@himalaya/wix-platform";
8
8
  import { payCashierOrder } from "@himalaya/checkout";
9
+ import { hostedCheckoutOpener, isHostedWorker } from "../_shared/hosted-checkout.js";
9
10
 
10
11
  /** Wix Stores appDefId — the vertical that owns a cart/ecom order, and therefore the
11
12
  * `appId` its cashier order is paid under. (Same constant Wix's own tooling uses.) */
@@ -317,9 +318,13 @@ const svc = defineService<State, {
317
318
  if (!checkout.checkoutId) return derive({ ...state, checkoutHint: copyParam("cart.checkoutIdMissingMessage", "Wix did not return a checkout id.") });
318
319
  const checkoutId = checkout.checkoutId;
319
320
 
320
- const order = await createOrder(checkoutId);
321
- const snapshotId = order.paymentGatewayOrderId;
322
- if (!snapshotId) {
321
+ // Hosted (Spaces): the site's own checkout page creates the order (RN's
322
+ // route) — creating it here first fails on a site with no payment method set up, before the
323
+ // member ever reaches the checkout. The host opens the checkout as the lent member.
324
+ const hosted = isHostedWorker();
325
+ const order = hosted ? { paymentGatewayOrderId: "" } : await createOrder(checkoutId);
326
+ const snapshotId = order.paymentGatewayOrderId ?? "";
327
+ if (!snapshotId && !hosted) {
323
328
  return derive({ ...state, ...cartConsumedByOrder() });
324
329
  }
325
330
 
@@ -335,6 +340,9 @@ const svc = defineService<State, {
335
340
  return !!reference && cartPricing.pricing.continueCheckoutGuard({ kind: "storesProduct", id: reference.catalogItemId }).allowed;
336
341
  });
337
342
  },
343
+ // Hosted (Spaces): the host opens the checkout as the lent member — Headless redirect
344
+ // sessions do not take a Member instance.
345
+ ...(hosted ? { openCashier: hostedCheckoutOpener(checkoutId) } : {}),
338
346
  cashierUrl: async () => {
339
347
  const redirect = await createRedirectSession({
340
348
  ecomCheckout: { checkoutId },
@@ -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
+ }
@@ -6,6 +6,7 @@ import { createCheckoutFromCurrentCart, createOrder, getCurrentCart, removeLineI
6
6
  import { createRedirectSession } from "@himalaya/wix-redirects";
7
7
  import { session as wixPlatformSession } from "@himalaya/wix-platform";
8
8
  import { payCashierOrder } from "@himalaya/checkout";
9
+ import { hostedCheckoutOpener, isHostedWorker } from "../_shared/hosted-checkout.js";
9
10
 
10
11
  /** Wix Stores appDefId — the vertical that owns a cart/ecom order, and therefore the
11
12
  * `appId` its cashier order is paid under. (Same constant Wix's own tooling uses.) */
@@ -317,9 +318,13 @@ const svc = defineService<State, {
317
318
  if (!checkout.checkoutId) return derive({ ...state, checkoutHint: copyParam("cart.checkoutIdMissingMessage", "Wix did not return a checkout id.") });
318
319
  const checkoutId = checkout.checkoutId;
319
320
 
320
- const order = await createOrder(checkoutId);
321
- const snapshotId = order.paymentGatewayOrderId;
322
- if (!snapshotId) {
321
+ // Hosted (Spaces): the site's own checkout page creates the order (RN's
322
+ // route) — creating it here first fails on a site with no payment method set up, before the
323
+ // member ever reaches the checkout. The host opens the checkout as the lent member.
324
+ const hosted = isHostedWorker();
325
+ const order = hosted ? { paymentGatewayOrderId: "" } : await createOrder(checkoutId);
326
+ const snapshotId = order.paymentGatewayOrderId ?? "";
327
+ if (!snapshotId && !hosted) {
323
328
  return derive({ ...state, ...cartConsumedByOrder() });
324
329
  }
325
330
 
@@ -335,6 +340,9 @@ const svc = defineService<State, {
335
340
  return !!reference && cartPricing.pricing.continueCheckoutGuard({ kind: "storesProduct", id: reference.catalogItemId }).allowed;
336
341
  });
337
342
  },
343
+ // Hosted (Spaces): the host opens the checkout as the lent member — Headless redirect
344
+ // sessions do not take a Member instance.
345
+ ...(hosted ? { openCashier: hostedCheckoutOpener(checkoutId) } : {}),
338
346
  cashierUrl: async () => {
339
347
  const redirect = await createRedirectSession({
340
348
  ecomCheckout: { checkoutId },
@@ -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 { bookSlot, getService, queryServiceAvailability, type WixBookingSlotFull
9
9
  import { createCheckout, createOrder } from "@himalaya/wix-ecom";
10
10
  import { createRedirectSession, type WixSlot } from "@himalaya/wix-redirects";
11
11
  import { payCashierOrder } from "@himalaya/checkout";
12
+ import { hostedCheckoutOpener, isHostedWorker } from "../_shared/hosted-checkout.js";
12
13
  import { session as wixPlatformSession } from "@himalaya/wix-platform";
13
14
  import { displayName, getMyData, type WixMyData } from "@himalaya/wix-users";
14
15
  import { formatServicePrice, serviceIsChargeable } from "../_shared/wix-service-price.js";
@@ -662,9 +663,12 @@ const svc = defineService<S, {
662
663
  // 3. Create the Wix-managed order — mints the cashier snapshot when a
663
664
  // balance is due. No paymentGatewayOrderId ⇒ nothing to charge
664
665
  // (covered / zero total) ⇒ the booking is already confirmed.
665
- const orderRes = await createOrder(checkoutId);
666
- const snapshotId = orderRes.paymentGatewayOrderId;
667
- if (!snapshotId) {
666
+ // Hosted (Spaces): the site's own checkout page creates the order (as
667
+ // RN does) — creating it here first fails on a site with no payment method set up.
668
+ const hosted = isHostedWorker();
669
+ const orderRes = hosted ? { paymentGatewayOrderId: "" } : await createOrder(checkoutId);
670
+ const snapshotId = orderRes.paymentGatewayOrderId ?? "";
671
+ if (!snapshotId && !hosted) {
668
672
  return derive({ ...pending, bookingInFlight: false, successMessage: copyParam("time.successMessage", "Booked. Your confirmation is ready in My Bookings."), message: "" });
669
673
  }
670
674
  // 4. Collect payment IN-APP — wallet when this app has one wired, otherwise
@@ -676,6 +680,8 @@ const svc = defineService<S, {
676
680
  appId: WIX_BOOKINGS_APP_ID,
677
681
  snapshotId,
678
682
  msid: await resolveMsid(),
683
+ // Hosted (Spaces): the host opens this booking's checkout as the lent member.
684
+ ...(hosted ? { openCashier: hostedCheckoutOpener(checkoutId) } : {}),
679
685
  cashierUrl: async () => {
680
686
  const redirect = await createRedirectSession({
681
687
  bookingsCheckout: { slotAvailability: { slot: slot as unknown as WixSlot } },