@wix/himalaya-cli 0.1346.0 → 0.1349.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 (20) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/checkout/src/index.d.ts +7 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/checkout.mjs +7 -2
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +7 -2
  4. package/dist/cli.mjs +32 -4
  5. package/package.json +2 -2
  6. package/templates/beauty-salon/tier5-src/_shared/hosted-checkout.ts +42 -0
  7. package/templates/beauty-salon/tier5-src/himi-booking-time/index.ts +9 -3
  8. package/templates/beauty-salon/tier5-src/himi-branded-lite-cart/index.ts +11 -3
  9. package/templates/branded-lite/tier5-src/_shared/hosted-checkout.ts +42 -0
  10. package/templates/branded-lite/tier5-src/himi-booking-time/index.ts +9 -3
  11. package/templates/branded-lite/tier5-src/himi-branded-lite-cart/index.ts +11 -3
  12. package/templates/gym-fitness/tier5-src/_shared/hosted-checkout.ts +42 -0
  13. package/templates/gym-fitness/tier5-src/himi-booking-time/index.ts +9 -3
  14. package/templates/gym-fitness/tier5-src/himi-branded-lite-cart/index.ts +11 -3
  15. package/templates/portfolio-creative/tier5-src/_shared/hosted-checkout.ts +42 -0
  16. package/templates/portfolio-creative/tier5-src/himi-branded-lite-cart/index.ts +11 -3
  17. package/templates/retail-store/tier5-src/_shared/hosted-checkout.ts +42 -0
  18. package/templates/retail-store/tier5-src/himi-branded-lite-cart/index.ts +11 -3
  19. package/templates/yoga-studio/tier5-src/_shared/hosted-checkout.ts +42 -0
  20. package/templates/yoga-studio/tier5-src/himi-booking-time/index.ts +9 -3
@@ -62,6 +62,13 @@ export interface PayRequest {
62
62
  currency?: string;
63
63
  /** Mints the in-app cashier URL. Called ONLY when the wallet rail cannot settle. */
64
64
  cashierUrl?: () => Promise<string | undefined>;
65
+ /** Opens the cashier ITSELF, in place of `cashierUrl` + an in-app browser: a host that must sign
66
+ * the member in on the site first (a hosted app's checkout, RFC 0071 §4 Step 4b) owns the URL,
67
+ * which carries a session token no worker may see. Resolves whether the cashier opened, or
68
+ * "withdrawn" when [stillMayCharge] — the caller's `mayCharge`, re-read — said no once the host had
69
+ * prepared it: preparing takes network round trips, during which the caller's route can change, so
70
+ * the host asks again right before it opens. */
71
+ openCashier?: (stillMayCharge: () => Promise<boolean>) => Promise<boolean | "withdrawn">;
65
72
  /** Re-read immediately before money moves (the wallet charge, the cashier opening). The caller's
66
73
  * own route can change while a rail is being chosen; false stops the flow with nothing charged. */
67
74
  mayCharge?: () => boolean | Promise<boolean>;
@@ -140,7 +140,7 @@ async function isEntitled() {
140
140
  }
141
141
  var WITHDRAWN = { kind: "failed", reason: "the caller withdrew the charge" };
142
142
  async function payOrder(req) {
143
- if (!req.snapshotId && !req.cashierUrl) {
143
+ if (!req.snapshotId && !req.cashierUrl && !req.openCashier) {
144
144
  return { kind: "failed", reason: "no order to settle" };
145
145
  }
146
146
  const availability = await isAvailable().catch(() => "unavailable");
@@ -163,7 +163,12 @@ async function payOrder(req) {
163
163
  }
164
164
  if (r.status === "cancelled") return { kind: "cancelled" };
165
165
  }
166
- if (req.cashierUrl) {
166
+ if (req.openCashier) {
167
+ if (await req.mayCharge?.() === false) return WITHDRAWN;
168
+ const opened = await req.openCashier(async () => await req.mayCharge?.() !== false).catch(() => false);
169
+ if (opened === "withdrawn") return WITHDRAWN;
170
+ if (opened === true) return { kind: "handoff" };
171
+ } else if (req.cashierUrl) {
167
172
  const url = await req.cashierUrl().catch(() => void 0);
168
173
  if (url) {
169
174
  if (await req.mayCharge?.() === false) return WITHDRAWN;
@@ -224,7 +224,7 @@ async function entitlement(planId) {
224
224
  }
225
225
  var WITHDRAWN = { kind: "failed", reason: "the caller withdrew the charge" };
226
226
  async function payOrder(req) {
227
- if (!req.snapshotId && !req.cashierUrl) {
227
+ if (!req.snapshotId && !req.cashierUrl && !req.openCashier) {
228
228
  return { kind: "failed", reason: "no order to settle" };
229
229
  }
230
230
  const availability = await isAvailable().catch(() => "unavailable");
@@ -247,7 +247,12 @@ async function payOrder(req) {
247
247
  }
248
248
  if (r.status === "cancelled") return { kind: "cancelled" };
249
249
  }
250
- if (req.cashierUrl) {
250
+ if (req.openCashier) {
251
+ if (await req.mayCharge?.() === false) return WITHDRAWN;
252
+ const opened = await req.openCashier(async () => await req.mayCharge?.() !== false).catch(() => false);
253
+ if (opened === "withdrawn") return WITHDRAWN;
254
+ if (opened === true) return { kind: "handoff" };
255
+ } else if (req.cashierUrl) {
251
256
  const url = await req.cashierUrl().catch(() => void 0);
252
257
  if (url) {
253
258
  if (await req.mayCharge?.() === false) return WITHDRAWN;
package/dist/cli.mjs CHANGED
@@ -7706,7 +7706,6 @@ function resolveTranslation(value, catalog3, sourceCatalog) {
7706
7706
  var BINDING_SIGILS, BINDING_SIGIL_RE, TRANSLATION_PREFIX, TRANSLATION_KEY_RE;
7707
7707
  var init_bindings = __esm({
7708
7708
  "../../core/ts/src/bindings.ts"() {
7709
- "use strict";
7710
7709
  BINDING_SIGILS = ["service", "flag", "item", "t"];
7711
7710
  BINDING_SIGIL_RE = new RegExp(
7712
7711
  `^\\$(?:${BINDING_SIGILS.filter((s2) => s2 !== "t").join("|")})\\b|^\\$t:`
@@ -22951,6 +22950,7 @@ __export(gen_native_config_exports, {
22951
22950
  LABEL_MODULES_ANDROID_INCLUDE: () => LABEL_MODULES_ANDROID_INCLUDE,
22952
22951
  LABEL_MODULES_ANDROID_INSTALLERS: () => LABEL_MODULES_ANDROID_INSTALLERS,
22953
22952
  LABEL_MODULES_ANDROID_LINK: () => LABEL_MODULES_ANDROID_LINK,
22953
+ LABEL_MODULES_IOS_FLAGS: () => LABEL_MODULES_IOS_FLAGS,
22954
22954
  LABEL_MODULES_IOS_LINK: () => LABEL_MODULES_IOS_LINK,
22955
22955
  LABEL_MODULES_IOS_PACKAGE: () => LABEL_MODULES_IOS_PACKAGE,
22956
22956
  LABEL_MODULES_SWIFT_IMPORT: () => LABEL_MODULES_SWIFT_IMPORT,
@@ -22963,7 +22963,9 @@ __export(gen_native_config_exports, {
22963
22963
  androidPlanFor: () => androidPlanFor,
22964
22964
  androidSettingsGradle: () => androidSettingsGradle,
22965
22965
  androidTvLeanbackPlan: () => androidTvLeanbackPlan,
22966
+ applyModuleFlags: () => applyModuleFlags,
22966
22967
  applyToFile: () => applyToFile,
22968
+ assertModuleFlagsMarkers: () => assertModuleFlagsMarkers,
22967
22969
  assertSingleMarkerPair: () => assertSingleMarkerPair,
22968
22970
  assertSingleModuleInstall: () => assertSingleModuleInstall,
22969
22971
  automotiveAppDescPath: () => automotiveAppDescPath,
@@ -22983,6 +22985,7 @@ __export(gen_native_config_exports, {
22983
22985
  iosPlanFor: () => iosPlanFor,
22984
22986
  kotlinBootSourcesUnder: () => kotlinBootSourcesUnder,
22985
22987
  kotlinFilesWithModuleInstall: () => kotlinFilesWithModuleInstall,
22988
+ linkedFlag: () => linkedFlag,
22986
22989
  loadOverrides: () => loadOverrides,
22987
22990
  macosPlanFor: () => macosPlanFor,
22988
22991
  moduleLinkRenderers: () => moduleLinkRenderers,
@@ -23001,6 +23004,7 @@ __export(gen_native_config_exports, {
23001
23004
  renderModulesAndroidInstallers: () => renderModulesAndroidInstallers,
23002
23005
  renderModulesAndroidLink: () => renderModulesAndroidLink,
23003
23006
  renderModulesAndroidLinkBinary: () => renderModulesAndroidLinkBinary,
23007
+ renderModulesIosFlags: () => renderModulesIosFlags,
23004
23008
  renderModulesIosLink: () => renderModulesIosLink,
23005
23009
  renderModulesIosLinkBinary: () => renderModulesIosLinkBinary,
23006
23010
  renderModulesIosPackage: () => renderModulesIosPackage,
@@ -23226,13 +23230,16 @@ function renderModulesAndroidLinkBinary(projects, coreVersion, indent) {
23226
23230
  function moduleLinkRenderers(modules, binary) {
23227
23231
  if (!binary) {
23228
23232
  return {
23233
+ iosLinked: () => modules.spm,
23229
23234
  iosPackage: (i2) => renderModulesIosPackage(modules.spm, i2),
23230
23235
  iosLink: (i2) => renderModulesIosLink(modules.spm, i2),
23231
23236
  androidInclude: (i2) => renderModulesAndroidInclude(modules.gradle, i2),
23232
23237
  androidLink: (i2) => renderModulesAndroidLink(modules.gradle, i2)
23233
23238
  };
23234
23239
  }
23240
+ const iosLinked = () => modules.spm.length && coreVersionHasModules(binary.coreVersion()) ? modules.spm : [];
23235
23241
  return {
23242
+ iosLinked,
23236
23243
  iosPackage: (i2) => modules.spm.length && coreVersionHasModules(binary.coreVersion()) ? renderModulesIosPackageBinary(modules.spm, binary.corePackagePath?.(), i2) : [],
23237
23244
  // Same version rule as Android: a pre-split core carries the capabilities and has no products.
23238
23245
  iosLink: (i2) => modules.spm.length && coreVersionHasModules(binary.coreVersion()) ? renderModulesIosLinkBinary(modules.spm, i2) : [],
@@ -23246,14 +23253,17 @@ function moduleLinkRenderers(modules, binary) {
23246
23253
  }
23247
23254
  };
23248
23255
  }
23256
+ function renderModulesIosFlags(products, indent) {
23257
+ return [`${indent}OTHER_SWIFT_FLAGS: "${["$(inherited)", ...products.map((p2) => `-D${linkedFlag(p2)}`)].join(" ")}"`];
23258
+ }
23249
23259
  function renderModulesSwiftImport(indent) {
23250
23260
  return NATIVE_MODULES.flatMap(
23251
- (m2) => m2.ios ? [`${indent}#if canImport(${m2.ios.product})`, `${indent}import ${m2.ios.product}`, `${indent}#endif`] : []
23261
+ (m2) => m2.ios ? [`${indent}#if canImport(${m2.ios.product}) && ${linkedFlag(m2.ios.product)}`, `${indent}import ${m2.ios.product}`, `${indent}#endif`] : []
23252
23262
  );
23253
23263
  }
23254
23264
  function renderModulesSwiftInstall(indent) {
23255
23265
  return NATIVE_MODULES.flatMap(
23256
- (m2) => m2.ios ? [`${indent}#if canImport(${m2.ios.product})`, `${indent}${m2.ios.product}.install()`, `${indent}#endif`] : []
23266
+ (m2) => m2.ios ? [`${indent}#if canImport(${m2.ios.product}) && ${linkedFlag(m2.ios.product)}`, `${indent}${m2.ios.product}.install()`, `${indent}#endif`] : []
23257
23267
  );
23258
23268
  }
23259
23269
  function renderModulesAndroidInstallers(indent) {
@@ -23433,6 +23443,19 @@ function applyToFile(path2, kind, label2, render, check) {
23433
23443
  writeFileSync13(path2, after);
23434
23444
  return { path: path2, outcome: "written" };
23435
23445
  }
23446
+ function assertModuleFlagsMarkers(paths) {
23447
+ for (const path2 of paths) {
23448
+ if (!existsSync34(path2)) continue;
23449
+ const text9 = readFileSync33(path2, "utf8");
23450
+ if (hasMarkers(text9, "yaml", LABEL_MODULES_IOS_LINK) && !hasMarkers(text9, "yaml", LABEL_MODULES_IOS_FLAGS)) {
23451
+ throw new Error(`${path2} links modules (${LABEL_MODULES_IOS_LINK}) but has no ${LABEL_MODULES_IOS_FLAGS} block under its project-level settings:, so no module would install. Add the markers (see apps/_template).`);
23452
+ }
23453
+ }
23454
+ }
23455
+ function applyModuleFlags(path2, products, check) {
23456
+ assertModuleFlagsMarkers([path2]);
23457
+ return applyToFile(path2, "yaml", LABEL_MODULES_IOS_FLAGS, (i2) => renderModulesIosFlags(products, i2), check);
23458
+ }
23436
23459
  function applyWholeFile(path2, content, check) {
23437
23460
  const current = existsSync34(path2) ? readFileSync33(path2, "utf8") : null;
23438
23461
  if (current === content) return { path: path2, outcome: "unchanged" };
@@ -23485,6 +23508,7 @@ function genNativeConfig(app, opts = {}) {
23485
23508
  const check = opts.check ?? false;
23486
23509
  assertSingleModuleInstall(app, swiftFilesWithModuleMarkers(app));
23487
23510
  assertSingleModuleInstall(app, kotlinFilesWithModuleInstall(app));
23511
+ assertModuleFlagsMarkers([iosProjectYml(app), macosProjectYml(app)]);
23488
23512
  const configPath = appConfigTs(app);
23489
23513
  const configTs = existsSync34(configPath) ? readFileSync33(configPath, "utf8") : "";
23490
23514
  const capabilities = parseStringArray(configTs, "capabilities");
@@ -23589,9 +23613,11 @@ function genNativeConfig(app, opts = {}) {
23589
23613
  // published AARs (#4055): no source package, no settings include.
23590
23614
  applyToFile(iosProjectYml(app), "yaml", LABEL_MODULES_IOS_PACKAGE, moduleLinks.iosPackage, check),
23591
23615
  applyToFile(iosProjectYml(app), "yaml", LABEL_MODULES_IOS_LINK, moduleLinks.iosLink, check),
23616
+ applyModuleFlags(iosProjectYml(app), moduleLinks.iosLinked(), check),
23592
23617
  // macOS (RFC 0046) reuses the iOS boot, so it links the same macOS-capable modules.
23593
23618
  applyToFile(macosProjectYml(app), "yaml", LABEL_MODULES_IOS_PACKAGE, (i2) => renderModulesIosPackage(modules.macos, i2), check),
23594
23619
  applyToFile(macosProjectYml(app), "yaml", LABEL_MODULES_IOS_LINK, (i2) => renderModulesIosLink(modules.macos, i2), check),
23620
+ applyModuleFlags(macosProjectYml(app), modules.macos, check),
23595
23621
  applyToFile(androidSettingsGradle(app), "slash", LABEL_MODULES_ANDROID_INCLUDE, moduleLinks.androidInclude, check),
23596
23622
  applyToFile(androidAppGradle(app), "slash", LABEL_MODULES_ANDROID_LINK, moduleLinks.androidLink, check),
23597
23623
  // watchOS (RFC 0047): the watch runtime mode/serve, declared once in config.ts. A watch
@@ -23685,7 +23711,7 @@ function genNativeConfig(app, opts = {}) {
23685
23711
  );
23686
23712
  return { app, buildSet: ids, files, drift: files.some((f2) => f2.outcome === "drift") };
23687
23713
  }
23688
- var LABEL_IOS_INFO, LABEL_IOS_ENTITLEMENTS, LABEL_ANDROID, LABEL_ANDROID_SERVICES, LABEL_MODULES_IOS_PACKAGE, LABEL_MODULES_IOS_LINK, LABEL_MODULES_ANDROID_INCLUDE, LABEL_MODULES_ANDROID_LINK, LABEL_MODULES_SWIFT_IMPORT, LABEL_MODULES_SWIFT_INSTALL, LABEL_MODULES_ANDROID_INSTALLERS, LABEL_WATCH, LABEL_TV, LABEL_ANDROID_TV, LABEL_ORIENTATION, LABEL_CAR, IOS_ORIENTATION, ALL_ORIENTATIONS, HEALTH_PERMISSION_COPY_META, FROM_APP_PLATFORM_DIR, CORE_ANDROID_CAPABILITY_MODULES, ANDROID_MAX_DEPTH, BOOT_SOURCE_MAX_BYTES, IOS_BUILD_CAPABILITIES_KEY, IOS_DIST_CAPABILITIES_KEY, ANDROID_BUILD_CAPABILITIES_META, ANDROID_DIST_CAPABILITIES_META;
23714
+ var LABEL_IOS_INFO, LABEL_IOS_ENTITLEMENTS, LABEL_ANDROID, LABEL_ANDROID_SERVICES, LABEL_MODULES_IOS_PACKAGE, LABEL_MODULES_IOS_LINK, LABEL_MODULES_ANDROID_INCLUDE, LABEL_MODULES_ANDROID_LINK, LABEL_MODULES_IOS_FLAGS, LABEL_MODULES_SWIFT_IMPORT, LABEL_MODULES_SWIFT_INSTALL, LABEL_MODULES_ANDROID_INSTALLERS, LABEL_WATCH, LABEL_TV, LABEL_ANDROID_TV, LABEL_ORIENTATION, LABEL_CAR, IOS_ORIENTATION, ALL_ORIENTATIONS, HEALTH_PERMISSION_COPY_META, FROM_APP_PLATFORM_DIR, linkedFlag, CORE_ANDROID_CAPABILITY_MODULES, ANDROID_MAX_DEPTH, BOOT_SOURCE_MAX_BYTES, IOS_BUILD_CAPABILITIES_KEY, IOS_DIST_CAPABILITIES_KEY, ANDROID_BUILD_CAPABILITIES_META, ANDROID_DIST_CAPABILITIES_META;
23689
23715
  var init_gen_native_config = __esm({
23690
23716
  "../native-config/gen-native-config.ts"() {
23691
23717
  init_src2();
@@ -23708,6 +23734,7 @@ var init_gen_native_config = __esm({
23708
23734
  LABEL_MODULES_IOS_LINK = "himalaya:modules:link";
23709
23735
  LABEL_MODULES_ANDROID_INCLUDE = "himalaya:modules:include";
23710
23736
  LABEL_MODULES_ANDROID_LINK = "himalaya:modules:dep";
23737
+ LABEL_MODULES_IOS_FLAGS = "himalaya:modules:flags";
23711
23738
  LABEL_MODULES_SWIFT_IMPORT = "himalaya:modules:import";
23712
23739
  LABEL_MODULES_SWIFT_INSTALL = "himalaya:modules:install";
23713
23740
  LABEL_MODULES_ANDROID_INSTALLERS = "himalaya:modules:installers";
@@ -23725,6 +23752,7 @@ var init_gen_native_config = __esm({
23725
23752
  ALL_ORIENTATIONS = ["portrait", "portraitUpsideDown", "landscapeLeft", "landscapeRight"];
23726
23753
  HEALTH_PERMISSION_COPY_META = "io.himalaya.health.permissionCopy";
23727
23754
  FROM_APP_PLATFORM_DIR = "../../../";
23755
+ linkedFlag = (product) => `HIMALAYA_LINKED_${product}`;
23728
23756
  CORE_ANDROID_CAPABILITY_MODULES = join34(
23729
23757
  dirname21(fileURLToPath14(import.meta.url)),
23730
23758
  "../../core/runtime/android/io/himalaya/core/src/main/kotlin/io/himalaya/core/CapabilityModules.kt"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1346.0",
3
+ "version": "0.1349.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": "d5621e3868f8ab4e19fa7e96c23ab61fd87096ddaa12ed3f7a1f8f31"
74
+ "falconPackageHash": "9279a00f8a3383291b5d42997cd4086dac34b4ccf1c4b62d3869dd44"
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 } },