@wix/himalaya-cli 0.1362.0 → 0.1364.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -7650,6 +7650,7 @@ function buttonVariant(style) {
7650
7650
  var SYSTEM_FONT_FAMILY, TONAL_ELEVATION_ALPHA, BUTTON_MIN_HIT_TARGET, nonNegative, nonEmpty, own, COLOR_TOKEN_PROPS, TYPOGRAPHY_TOKEN_PROPS, BUTTON_VARIANTS;
7651
7651
  var init_tokens = __esm({
7652
7652
  "../../core/ts/src/tokens.ts"() {
7653
+ "use strict";
7653
7654
  SYSTEM_FONT_FAMILY = "$system";
7654
7655
  TONAL_ELEVATION_ALPHA = [0, 0.05, 0.08, 0.11, 0.12, 0.14];
7655
7656
  BUTTON_MIN_HIT_TARGET = 44;
@@ -7705,6 +7706,7 @@ function resolveTranslation(value, catalog3, sourceCatalog) {
7705
7706
  var BINDING_SIGILS, BINDING_SIGIL_RE, TRANSLATION_PREFIX, TRANSLATION_KEY_RE;
7706
7707
  var init_bindings = __esm({
7707
7708
  "../../core/ts/src/bindings.ts"() {
7709
+ "use strict";
7708
7710
  BINDING_SIGILS = ["service", "flag", "item", "t"];
7709
7711
  BINDING_SIGIL_RE = new RegExp(
7710
7712
  `^\\$(?:${BINDING_SIGILS.filter((s2) => s2 !== "t").join("|")})\\b|^\\$t:`
@@ -13544,12 +13546,10 @@ var init_player_host_manifest = __esm({
13544
13546
  ],
13545
13547
  "unlinked": {
13546
13548
  "ios": [
13547
- "base44",
13548
13549
  "godot"
13549
13550
  ],
13550
13551
  "macos": [
13551
13552
  "audio_record_background",
13552
- "base44",
13553
13553
  "browser_auth",
13554
13554
  "godot",
13555
13555
  "iap",
@@ -13560,7 +13560,6 @@ var init_player_host_manifest = __esm({
13560
13560
  "wix_cashier"
13561
13561
  ],
13562
13562
  "android": [
13563
- "base44",
13564
13563
  "godot"
13565
13564
  ],
13566
13565
  "web": []
@@ -24884,7 +24883,7 @@ function assessPackage(header, host) {
24884
24883
  if (caps.length) out2.push({ reason: "missing_capabilities", missing: caps });
24885
24884
  const comps = missing(req.nativeComponents ?? [], host.nativeComponents);
24886
24885
  if (comps.length) out2.push({ reason: "missing_components", missing: comps });
24887
- const auth = missing(req.authProviders ?? [], host.authProviders);
24886
+ const auth = missing((req.authProviders ?? []).filter((id) => id !== "none"), host.authProviders);
24888
24887
  if (auth.length) out2.push({ reason: "missing_auth_providers", missing: auth });
24889
24888
  }
24890
24889
  return out2;
@@ -28037,8 +28036,8 @@ var init_shell_capabilities_generated = __esm({
28037
28036
  appDir: "apps/himi-player",
28038
28037
  shared: true,
28039
28038
  extensible: false,
28040
- built: ["animation", "audio_record_background", "biometrics", "calendar", "calls", "camera", "clipboard", "contacts", "haptic", "iap", "location", "media_playback", "motion", "on_device_ai", "photos", "push", "speech"],
28041
- serves: ["animation", "app_info", "audio_record", "audio_record_background", "biometrics", "browser_auth", "calendar", "calls", "camera", "clipboard", "contacts", "file_picker", "files", "haptic", "himi_apps", "iap", "keyboard", "local_database", "location", "media_playback", "motion", "on_device_ai", "open_url", "pdf", "photos", "presigned_upload", "push", "release_control", "secure_storage", "share", "speaker_diarization", "speech", "stream", "theme", "ui", "wix_api", "wix_cashier", "wix_platform"]
28039
+ built: ["animation", "audio_record_background", "base44", "biometrics", "calendar", "calls", "camera", "clipboard", "contacts", "haptic", "iap", "location", "media_playback", "motion", "on_device_ai", "photos", "push", "speech"],
28040
+ serves: ["animation", "app_info", "audio_record", "audio_record_background", "base44", "biometrics", "browser_auth", "calendar", "calls", "camera", "clipboard", "contacts", "file_picker", "files", "haptic", "himi_apps", "iap", "keyboard", "local_database", "location", "media_playback", "motion", "on_device_ai", "open_url", "pdf", "photos", "presigned_upload", "push", "release_control", "secure_storage", "share", "speaker_diarization", "speech", "stream", "theme", "ui", "wix_api", "wix_cashier", "wix_platform"]
28042
28041
  }
28043
28042
  };
28044
28043
  }
@@ -137339,7 +137338,9 @@ async function buildFromLoaded(preloaded, dest, opts = {}) {
137339
137338
  playerRequirements: {
137340
137339
  capabilities: [...requiredCapabilities].sort(),
137341
137340
  nativeComponents: [...requiredNativeComponents].sort(),
137342
- authProviders: [config.auth.providerId].filter(Boolean).sort()
137341
+ // `none` is the scaffold's spelling of "no auth" (`new-app --auth=none`), not a provider a
137342
+ // host must carry — recording it blocked every such app in the Player picker (#3695).
137343
+ authProviders: [config.auth.providerId].filter((id) => id && id !== "none").sort()
137343
137344
  },
137344
137345
  // #4091 — the app takes its Wix identity from a HOST (Spaces Lite) instead of signing in
137345
137346
  // itself (#4088 `hostProvided` on its auth provider). A host that lends its identity only
@@ -169974,7 +169975,8 @@ function missingClaims(requirements, offered) {
169974
169975
  const out2 = {};
169975
169976
  for (const kind of HOST_CLAIM_KINDS) {
169976
169977
  const has = new Set(offered[kind]);
169977
- out2[kind] = [...new Set(requirements[kind].filter((id) => !has.has(id)))];
169978
+ const required3 = kind === "authProviders" ? requirements[kind].filter((id) => id !== "none") : requirements[kind];
169979
+ out2[kind] = [...new Set(required3.filter((id) => !has.has(id)))];
169978
169980
  }
169979
169981
  return out2;
169980
169982
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1362.0",
3
+ "version": "0.1364.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": "6f8e9ebff2033817c8121233af0096191f0d147fc9d49e65d9e6f02d"
74
+ "falconPackageHash": "d3c28af7c96bcc51af2111ceb585221c63a75150cacb4a2e9ec7facc"
75
75
  }
@@ -9,7 +9,7 @@ import { hostSessionFromGlobal, iapPriceFields, type IapCatalogItem } from "@him
9
9
  import { createIapCommerce, pricingPlanOrders } from "@himalaya/iap-checkout";
10
10
  import { iapPriceCopy, iapPurchaseCopy, resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
11
11
  import { formatCurrency } from "../_shared/engine-intl.js";
12
- import { isHostedWorker } from "../_shared/hosted-checkout.js";
12
+ import { isHostedWorker, hostedSitePageOpener } from "../_shared/hosted-checkout.js";
13
13
 
14
14
  /** Wix Pricing Plans appDefId — the vertical that owns a plan order, so the `appId` its
15
15
  * cashier order is paid under (mirrors AppDefId.PRICING_PLANS in Wix's RN apps). */
@@ -186,6 +186,8 @@ let started = false;
186
186
  let loading = 0;
187
187
  let contentReady = false;
188
188
  const joining = new Set<string>();
189
+ /** A hosted paid plan's page is being prepared by the host (one ticket at a time). */
190
+ let hostedPlanOpenInFlight = false;
189
191
  const memberSession = hostSessionFromGlobal();
190
192
  let regularGeneration = -1;
191
193
  let regularAccess = new Set<string>();
@@ -227,9 +229,27 @@ async function regularJoinMessage(row: PlanRow, reference: { kind: string; id: s
227
229
  // Hosted inside a host shell (Spaces) a plan cannot be paid in-app: classic Pricing Plans is not on
228
230
  // ecom, so the host's member checkout has no checkout to open, and the redirect session needs a
229
231
  // headless OAuth client a hosted app does not have. An online order created here would only be
230
- // left behind as a DRAFT, so none is — the member joins on the site. A FREE plan has nothing to
231
- // pay: its online order activates at once, so it still joins in-app.
232
+ // left behind as a DRAFT, so none is — the member joins on the site: the HOST opens the site's own
233
+ // plans page signed in as the lent member (`himi.host.memberSitePage {page:"plans"}`), or this
234
+ // message stands when it cannot. A FREE plan has nothing to pay: its online order activates at
235
+ // once, so it still joins in-app.
232
236
  if (isHostedWorker() && !row.free) {
237
+ // One host open at a time: the host keeps a single ticket, so a second plan's prepare would replace the
238
+ // first's and drop it. The checkout guard is re-read right before the host opens the page.
239
+ if (hostedPlanOpenInFlight) return "";
240
+ hostedPlanOpenInFlight = true;
241
+ let opened: boolean | "withdrawn";
242
+ try {
243
+ opened = await hostedSitePageOpener({ page: "plans", planId: row.id })(
244
+ async () => commerce.pricing.continueCheckoutGuard(reference).allowed);
245
+ } finally {
246
+ hostedPlanOpenInFlight = false;
247
+ }
248
+ if (generation !== memberSession.current().generation) return "";
249
+ if (opened === "withdrawn") return "";
250
+ if (opened === true) {
251
+ return copyTemplate("plans.continueOnSiteMessage", "Finish joining {name} on the site.", { name: row.name });
252
+ }
233
253
  return copyTemplate("plans.joinOnSiteMessage", "Memberships are purchased on the website — visit the site to join {name}.", { name: row.name });
234
254
  }
235
255
  try {
@@ -9,7 +9,7 @@ import { hostSessionFromGlobal, iapPriceFields, type IapCatalogItem } from "@him
9
9
  import { createIapCommerce, pricingPlanOrders } from "@himalaya/iap-checkout";
10
10
  import { iapPriceCopy, iapPurchaseCopy, resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
11
11
  import { formatCurrency } from "../_shared/engine-intl.js";
12
- import { isHostedWorker } from "../_shared/hosted-checkout.js";
12
+ import { isHostedWorker, hostedSitePageOpener } from "../_shared/hosted-checkout.js";
13
13
 
14
14
  /** Wix Pricing Plans appDefId — the vertical that owns a plan order, so the `appId` its
15
15
  * cashier order is paid under (mirrors AppDefId.PRICING_PLANS in Wix's RN apps). */
@@ -186,6 +186,8 @@ let started = false;
186
186
  let loading = 0;
187
187
  let contentReady = false;
188
188
  const joining = new Set<string>();
189
+ /** A hosted paid plan's page is being prepared by the host (one ticket at a time). */
190
+ let hostedPlanOpenInFlight = false;
189
191
  const memberSession = hostSessionFromGlobal();
190
192
  let regularGeneration = -1;
191
193
  let regularAccess = new Set<string>();
@@ -227,9 +229,27 @@ async function regularJoinMessage(row: PlanRow, reference: { kind: string; id: s
227
229
  // Hosted inside a host shell (Spaces) a plan cannot be paid in-app: classic Pricing Plans is not on
228
230
  // ecom, so the host's member checkout has no checkout to open, and the redirect session needs a
229
231
  // headless OAuth client a hosted app does not have. An online order created here would only be
230
- // left behind as a DRAFT, so none is — the member joins on the site. A FREE plan has nothing to
231
- // pay: its online order activates at once, so it still joins in-app.
232
+ // left behind as a DRAFT, so none is — the member joins on the site: the HOST opens the site's own
233
+ // plans page signed in as the lent member (`himi.host.memberSitePage {page:"plans"}`), or this
234
+ // message stands when it cannot. A FREE plan has nothing to pay: its online order activates at
235
+ // once, so it still joins in-app.
232
236
  if (isHostedWorker() && !row.free) {
237
+ // One host open at a time: the host keeps a single ticket, so a second plan's prepare would replace the
238
+ // first's and drop it. The checkout guard is re-read right before the host opens the page.
239
+ if (hostedPlanOpenInFlight) return "";
240
+ hostedPlanOpenInFlight = true;
241
+ let opened: boolean | "withdrawn";
242
+ try {
243
+ opened = await hostedSitePageOpener({ page: "plans", planId: row.id })(
244
+ async () => commerce.pricing.continueCheckoutGuard(reference).allowed);
245
+ } finally {
246
+ hostedPlanOpenInFlight = false;
247
+ }
248
+ if (generation !== memberSession.current().generation) return "";
249
+ if (opened === "withdrawn") return "";
250
+ if (opened === true) {
251
+ return copyTemplate("plans.continueOnSiteMessage", "Finish joining {name} on the site.", { name: row.name });
252
+ }
233
253
  return copyTemplate("plans.joinOnSiteMessage", "Memberships are purchased on the website — visit the site to join {name}.", { name: row.name });
234
254
  }
235
255
  try {
@@ -9,7 +9,7 @@ import { hostSessionFromGlobal, iapPriceFields, type IapCatalogItem } from "@him
9
9
  import { createIapCommerce, pricingPlanOrders } from "@himalaya/iap-checkout";
10
10
  import { iapPriceCopy, iapPurchaseCopy, resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
11
11
  import { formatCurrency } from "../_shared/engine-intl.js";
12
- import { isHostedWorker } from "../_shared/hosted-checkout.js";
12
+ import { isHostedWorker, hostedSitePageOpener } from "../_shared/hosted-checkout.js";
13
13
 
14
14
  /** Wix Pricing Plans appDefId — the vertical that owns a plan order, so the `appId` its
15
15
  * cashier order is paid under (mirrors AppDefId.PRICING_PLANS in Wix's RN apps). */
@@ -186,6 +186,8 @@ let started = false;
186
186
  let loading = 0;
187
187
  let contentReady = false;
188
188
  const joining = new Set<string>();
189
+ /** A hosted paid plan's page is being prepared by the host (one ticket at a time). */
190
+ let hostedPlanOpenInFlight = false;
189
191
  const memberSession = hostSessionFromGlobal();
190
192
  let regularGeneration = -1;
191
193
  let regularAccess = new Set<string>();
@@ -227,9 +229,27 @@ async function regularJoinMessage(row: PlanRow, reference: { kind: string; id: s
227
229
  // Hosted inside a host shell (Spaces) a plan cannot be paid in-app: classic Pricing Plans is not on
228
230
  // ecom, so the host's member checkout has no checkout to open, and the redirect session needs a
229
231
  // headless OAuth client a hosted app does not have. An online order created here would only be
230
- // left behind as a DRAFT, so none is — the member joins on the site. A FREE plan has nothing to
231
- // pay: its online order activates at once, so it still joins in-app.
232
+ // left behind as a DRAFT, so none is — the member joins on the site: the HOST opens the site's own
233
+ // plans page signed in as the lent member (`himi.host.memberSitePage {page:"plans"}`), or this
234
+ // message stands when it cannot. A FREE plan has nothing to pay: its online order activates at
235
+ // once, so it still joins in-app.
232
236
  if (isHostedWorker() && !row.free) {
237
+ // One host open at a time: the host keeps a single ticket, so a second plan's prepare would replace the
238
+ // first's and drop it. The checkout guard is re-read right before the host opens the page.
239
+ if (hostedPlanOpenInFlight) return "";
240
+ hostedPlanOpenInFlight = true;
241
+ let opened: boolean | "withdrawn";
242
+ try {
243
+ opened = await hostedSitePageOpener({ page: "plans", planId: row.id })(
244
+ async () => commerce.pricing.continueCheckoutGuard(reference).allowed);
245
+ } finally {
246
+ hostedPlanOpenInFlight = false;
247
+ }
248
+ if (generation !== memberSession.current().generation) return "";
249
+ if (opened === "withdrawn") return "";
250
+ if (opened === true) {
251
+ return copyTemplate("plans.continueOnSiteMessage", "Finish joining {name} on the site.", { name: row.name });
252
+ }
233
253
  return copyTemplate("plans.joinOnSiteMessage", "Memberships are purchased on the website — visit the site to join {name}.", { name: row.name });
234
254
  }
235
255
  try {
@@ -9,7 +9,7 @@ import { hostSessionFromGlobal, iapPriceFields, type IapCatalogItem } from "@him
9
9
  import { createIapCommerce, pricingPlanOrders } from "@himalaya/iap-checkout";
10
10
  import { iapPriceCopy, iapPurchaseCopy, resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
11
11
  import { formatCurrency } from "../_shared/engine-intl.js";
12
- import { isHostedWorker } from "../_shared/hosted-checkout.js";
12
+ import { isHostedWorker, hostedSitePageOpener } from "../_shared/hosted-checkout.js";
13
13
 
14
14
  /** Wix Pricing Plans appDefId — the vertical that owns a plan order, so the `appId` its
15
15
  * cashier order is paid under (mirrors AppDefId.PRICING_PLANS in Wix's RN apps). */
@@ -186,6 +186,8 @@ let started = false;
186
186
  let loading = 0;
187
187
  let contentReady = false;
188
188
  const joining = new Set<string>();
189
+ /** A hosted paid plan's page is being prepared by the host (one ticket at a time). */
190
+ let hostedPlanOpenInFlight = false;
189
191
  const memberSession = hostSessionFromGlobal();
190
192
  let regularGeneration = -1;
191
193
  let regularAccess = new Set<string>();
@@ -227,9 +229,27 @@ async function regularJoinMessage(row: PlanRow, reference: { kind: string; id: s
227
229
  // Hosted inside a host shell (Spaces) a plan cannot be paid in-app: classic Pricing Plans is not on
228
230
  // ecom, so the host's member checkout has no checkout to open, and the redirect session needs a
229
231
  // headless OAuth client a hosted app does not have. An online order created here would only be
230
- // left behind as a DRAFT, so none is — the member joins on the site. A FREE plan has nothing to
231
- // pay: its online order activates at once, so it still joins in-app.
232
+ // left behind as a DRAFT, so none is — the member joins on the site: the HOST opens the site's own
233
+ // plans page signed in as the lent member (`himi.host.memberSitePage {page:"plans"}`), or this
234
+ // message stands when it cannot. A FREE plan has nothing to pay: its online order activates at
235
+ // once, so it still joins in-app.
232
236
  if (isHostedWorker() && !row.free) {
237
+ // One host open at a time: the host keeps a single ticket, so a second plan's prepare would replace the
238
+ // first's and drop it. The checkout guard is re-read right before the host opens the page.
239
+ if (hostedPlanOpenInFlight) return "";
240
+ hostedPlanOpenInFlight = true;
241
+ let opened: boolean | "withdrawn";
242
+ try {
243
+ opened = await hostedSitePageOpener({ page: "plans", planId: row.id })(
244
+ async () => commerce.pricing.continueCheckoutGuard(reference).allowed);
245
+ } finally {
246
+ hostedPlanOpenInFlight = false;
247
+ }
248
+ if (generation !== memberSession.current().generation) return "";
249
+ if (opened === "withdrawn") return "";
250
+ if (opened === true) {
251
+ return copyTemplate("plans.continueOnSiteMessage", "Finish joining {name} on the site.", { name: row.name });
252
+ }
233
253
  return copyTemplate("plans.joinOnSiteMessage", "Memberships are purchased on the website — visit the site to join {name}.", { name: row.name });
234
254
  }
235
255
  try {
@@ -9,7 +9,7 @@ import { hostSessionFromGlobal, iapPriceFields, type IapCatalogItem } from "@him
9
9
  import { createIapCommerce, pricingPlanOrders } from "@himalaya/iap-checkout";
10
10
  import { iapPriceCopy, iapPurchaseCopy, resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
11
11
  import { formatCurrency } from "../_shared/engine-intl.js";
12
- import { isHostedWorker } from "../_shared/hosted-checkout.js";
12
+ import { isHostedWorker, hostedSitePageOpener } from "../_shared/hosted-checkout.js";
13
13
 
14
14
  /** Wix Pricing Plans appDefId — the vertical that owns a plan order, so the `appId` its
15
15
  * cashier order is paid under (mirrors AppDefId.PRICING_PLANS in Wix's RN apps). */
@@ -186,6 +186,8 @@ let started = false;
186
186
  let loading = 0;
187
187
  let contentReady = false;
188
188
  const joining = new Set<string>();
189
+ /** A hosted paid plan's page is being prepared by the host (one ticket at a time). */
190
+ let hostedPlanOpenInFlight = false;
189
191
  const memberSession = hostSessionFromGlobal();
190
192
  let regularGeneration = -1;
191
193
  let regularAccess = new Set<string>();
@@ -227,9 +229,27 @@ async function regularJoinMessage(row: PlanRow, reference: { kind: string; id: s
227
229
  // Hosted inside a host shell (Spaces) a plan cannot be paid in-app: classic Pricing Plans is not on
228
230
  // ecom, so the host's member checkout has no checkout to open, and the redirect session needs a
229
231
  // headless OAuth client a hosted app does not have. An online order created here would only be
230
- // left behind as a DRAFT, so none is — the member joins on the site. A FREE plan has nothing to
231
- // pay: its online order activates at once, so it still joins in-app.
232
+ // left behind as a DRAFT, so none is — the member joins on the site: the HOST opens the site's own
233
+ // plans page signed in as the lent member (`himi.host.memberSitePage {page:"plans"}`), or this
234
+ // message stands when it cannot. A FREE plan has nothing to pay: its online order activates at
235
+ // once, so it still joins in-app.
232
236
  if (isHostedWorker() && !row.free) {
237
+ // One host open at a time: the host keeps a single ticket, so a second plan's prepare would replace the
238
+ // first's and drop it. The checkout guard is re-read right before the host opens the page.
239
+ if (hostedPlanOpenInFlight) return "";
240
+ hostedPlanOpenInFlight = true;
241
+ let opened: boolean | "withdrawn";
242
+ try {
243
+ opened = await hostedSitePageOpener({ page: "plans", planId: row.id })(
244
+ async () => commerce.pricing.continueCheckoutGuard(reference).allowed);
245
+ } finally {
246
+ hostedPlanOpenInFlight = false;
247
+ }
248
+ if (generation !== memberSession.current().generation) return "";
249
+ if (opened === "withdrawn") return "";
250
+ if (opened === true) {
251
+ return copyTemplate("plans.continueOnSiteMessage", "Finish joining {name} on the site.", { name: row.name });
252
+ }
233
253
  return copyTemplate("plans.joinOnSiteMessage", "Memberships are purchased on the website — visit the site to join {name}.", { name: row.name });
234
254
  }
235
255
  try {
@@ -9,7 +9,7 @@ import { hostSessionFromGlobal, iapPriceFields, type IapCatalogItem } from "@him
9
9
  import { createIapCommerce, pricingPlanOrders } from "@himalaya/iap-checkout";
10
10
  import { iapPriceCopy, iapPurchaseCopy, resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
11
11
  import { formatCurrency } from "../_shared/engine-intl.js";
12
- import { isHostedWorker } from "../_shared/hosted-checkout.js";
12
+ import { isHostedWorker, hostedSitePageOpener } from "../_shared/hosted-checkout.js";
13
13
 
14
14
  /** Wix Pricing Plans appDefId — the vertical that owns a plan order, so the `appId` its
15
15
  * cashier order is paid under (mirrors AppDefId.PRICING_PLANS in Wix's RN apps). */
@@ -186,6 +186,8 @@ let started = false;
186
186
  let loading = 0;
187
187
  let contentReady = false;
188
188
  const joining = new Set<string>();
189
+ /** A hosted paid plan's page is being prepared by the host (one ticket at a time). */
190
+ let hostedPlanOpenInFlight = false;
189
191
  const memberSession = hostSessionFromGlobal();
190
192
  let regularGeneration = -1;
191
193
  let regularAccess = new Set<string>();
@@ -227,9 +229,27 @@ async function regularJoinMessage(row: PlanRow, reference: { kind: string; id: s
227
229
  // Hosted inside a host shell (Spaces) a plan cannot be paid in-app: classic Pricing Plans is not on
228
230
  // ecom, so the host's member checkout has no checkout to open, and the redirect session needs a
229
231
  // headless OAuth client a hosted app does not have. An online order created here would only be
230
- // left behind as a DRAFT, so none is — the member joins on the site. A FREE plan has nothing to
231
- // pay: its online order activates at once, so it still joins in-app.
232
+ // left behind as a DRAFT, so none is — the member joins on the site: the HOST opens the site's own
233
+ // plans page signed in as the lent member (`himi.host.memberSitePage {page:"plans"}`), or this
234
+ // message stands when it cannot. A FREE plan has nothing to pay: its online order activates at
235
+ // once, so it still joins in-app.
232
236
  if (isHostedWorker() && !row.free) {
237
+ // One host open at a time: the host keeps a single ticket, so a second plan's prepare would replace the
238
+ // first's and drop it. The checkout guard is re-read right before the host opens the page.
239
+ if (hostedPlanOpenInFlight) return "";
240
+ hostedPlanOpenInFlight = true;
241
+ let opened: boolean | "withdrawn";
242
+ try {
243
+ opened = await hostedSitePageOpener({ page: "plans", planId: row.id })(
244
+ async () => commerce.pricing.continueCheckoutGuard(reference).allowed);
245
+ } finally {
246
+ hostedPlanOpenInFlight = false;
247
+ }
248
+ if (generation !== memberSession.current().generation) return "";
249
+ if (opened === "withdrawn") return "";
250
+ if (opened === true) {
251
+ return copyTemplate("plans.continueOnSiteMessage", "Finish joining {name} on the site.", { name: row.name });
252
+ }
233
253
  return copyTemplate("plans.joinOnSiteMessage", "Memberships are purchased on the website — visit the site to join {name}.", { name: row.name });
234
254
  }
235
255
  try {