@wix/himalaya-cli 0.1354.0 → 0.1356.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 (22) hide show
  1. package/dist/cli.mjs +2 -0
  2. package/package.json +2 -2
  3. package/templates/beauty-salon/tier5-src/_shared/hosted-checkout.ts +36 -10
  4. package/templates/branded-lite/tier5-src/_shared/hosted-checkout.ts +36 -10
  5. package/templates/branded-lite/tier5-src/himi-events-detail/index.ts +31 -2
  6. package/templates/community-club/tier5-src/_shared/hosted-checkout.ts +36 -10
  7. package/templates/community-club/tier5-src/himi-events-detail/index.ts +31 -2
  8. package/templates/events-meetups/tier5-src/_shared/hosted-checkout.ts +36 -10
  9. package/templates/events-meetups/tier5-src/himi-events-detail/index.ts +31 -2
  10. package/templates/gym-fitness/tier5-src/_shared/hosted-checkout.ts +36 -10
  11. package/templates/gym-fitness/tier5-src/himi-events-detail/index.ts +31 -2
  12. package/templates/nonprofit-cause/tier5-src/_shared/hosted-checkout.ts +36 -10
  13. package/templates/nonprofit-cause/tier5-src/himi-events-detail/index.ts +31 -2
  14. package/templates/podcast-creator/tier5-src/_shared/hosted-checkout.ts +36 -10
  15. package/templates/podcast-creator/tier5-src/himi-events-detail/index.ts +31 -2
  16. package/templates/portfolio-creative/tier5-src/_shared/hosted-checkout.ts +36 -10
  17. package/templates/portfolio-creative/tier5-src/himi-events-detail/index.ts +31 -2
  18. package/templates/restaurant/tier5-src/_shared/hosted-checkout.ts +36 -10
  19. package/templates/restaurant/tier5-src/himi-events-detail/index.ts +31 -2
  20. package/templates/retail-store/tier5-src/_shared/hosted-checkout.ts +36 -10
  21. package/templates/yoga-studio/tier5-src/_shared/hosted-checkout.ts +36 -10
  22. package/templates/yoga-studio/tier5-src/himi-events-detail/index.ts +31 -2
package/dist/cli.mjs CHANGED
@@ -7706,6 +7706,7 @@ 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";
7709
7710
  BINDING_SIGILS = ["service", "flag", "item", "t"];
7710
7711
  BINDING_SIGIL_RE = new RegExp(
7711
7712
  `^\\$(?:${BINDING_SIGILS.filter((s2) => s2 !== "t").join("|")})\\b|^\\$t:`
@@ -16744,6 +16745,7 @@ function resolveIdiom(tokens2, ctx = {}) {
16744
16745
  var IDIOM_CONTEXTS, UNSAFE_KEYS;
16745
16746
  var init_idiom = __esm({
16746
16747
  "../../core/ts/src/idiom.ts"() {
16748
+ "use strict";
16747
16749
  IDIOM_CONTEXTS = {
16748
16750
  ios: { native: ["ios"] },
16749
16751
  android: { native: ["material"] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1354.0",
3
+ "version": "0.1356.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": "07e69214f20355a26a4e813ff46d40f5f5e4f8eaff658f0598038078"
74
+ "falconPackageHash": "3a06b1f197d8ef760b44c7d9cca4118a909cfea2ac374a6cd07362a1"
75
75
  }
@@ -1,8 +1,12 @@
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.
1
+ // Hosted checkout. A Branded Lite hosted in a host shell (Spaces) pays as the member the host
2
+ // lent it: Headless `createRedirectSession` does not take a Member instance, so the HOST opens the site's
3
+ // checkout signed in as that member. The worker names only its ecom checkout; the host reads Wix's
4
+ // checkout URL, mints a one-time member session and opens the site's `createSessionCookie` → checkout.
5
+ // The session token never reaches this worker.
6
+ //
7
+ // Classic Wix Events tickets and classic Pricing Plans have no ecom checkout: for those the
8
+ // worker names the page by id (`himi.host.memberSitePage`) and the host opens the site's own event or
9
+ // plans page the same way, signed in — where the member picks and pays on the site.
6
10
 
7
11
  import { dispatch } from "@himalaya/dispatch";
8
12
  import { hostSiteFromInput } from "./branded-lite-hosted.js";
@@ -13,6 +17,12 @@ import { hostSiteFromInput } from "./branded-lite-hosted.js";
13
17
  * URL (which carries the member's session token) stays in the host; only the ticket crosses here. */
14
18
  export const HOSTED_CHECKOUT_ACTION = "himi.host.memberCheckout";
15
19
  export const HOSTED_CHECKOUT_OPEN_ACTION = "himi.host.memberCheckout.open";
20
+ /** Prepares a site PAGE (not an ecom checkout) the same way; opened with [HOSTED_CHECKOUT_OPEN_ACTION]. */
21
+ export const HOSTED_SITE_PAGE_ACTION = "himi.host.memberSitePage";
22
+
23
+ /** A site page a hosted worker may ask its host for — ids only, never a URL: the host reads the URL
24
+ * from Wix itself and decides whether the member may be signed in there. */
25
+ export type HostedSitePage = { page: "event"; eventId: string } | { page: "plans"; planId?: string };
16
26
 
17
27
  /** Whether this worker runs inside a host that opened a site for it. */
18
28
  export function isHostedWorker(): boolean {
@@ -20,14 +30,14 @@ export function isHostedWorker(): boolean {
20
30
  }
21
31
 
22
32
  type HostAnswer = { opened?: unknown; ticket?: unknown; payload?: { opened?: unknown; ticket?: unknown } } | null;
33
+ type HostOpener = (stillMayCharge: () => Promise<boolean>) => Promise<boolean | "withdrawn">;
23
34
 
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"> {
35
+ /** Prepare with [action] [params] → re-read the guard → open by ticket. A host without the verbs, or one
36
+ * that refused (Wix named no page, an unsafe URL, no account session), → false, never a throw. */
37
+ function hostOpener(action: string, params: Record<string, string>): HostOpener {
28
38
  return async (stillMayCharge) => {
29
39
  try {
30
- const prepared = await dispatch(HOSTED_CHECKOUT_ACTION, { checkoutId }) as HostAnswer;
40
+ const prepared = await dispatch(action, params) as HostAnswer;
31
41
  const ticket = prepared?.ticket ?? prepared?.payload?.ticket;
32
42
  if (typeof ticket !== "string" || !ticket) return false;
33
43
  // An unopened ticket simply expires in the host: nothing to cancel.
@@ -40,3 +50,19 @@ export function hostedCheckoutOpener(checkoutId: string): (stillMayCharge: () =>
40
50
  }
41
51
  };
42
52
  }
53
+
54
+ /** `payCashierOrder`'s `openCashier` for a hosted app: the host prepares [checkoutId]'s checkout as the
55
+ * lent member, the caller's guard is re-read, and only then does the host open it. Resolves whether it
56
+ * opened ("withdrawn" when the guard said no); a host without the verbs (or that refused) → false. */
57
+ export function hostedCheckoutOpener(checkoutId: string): HostOpener {
58
+ return hostOpener(HOSTED_CHECKOUT_ACTION, { checkoutId });
59
+ }
60
+
61
+ /** The site's own [page] opened by the host, signed in as the lent member where the host deems it safe
62
+ * (a visitor otherwise) — the same two steps and answers as [hostedCheckoutOpener]. */
63
+ export function hostedSitePageOpener(page: HostedSitePage): HostOpener {
64
+ const params: Record<string, string> = page.page === "event"
65
+ ? { page: "event", eventId: page.eventId }
66
+ : { page: "plans", ...(page.planId ? { planId: page.planId } : {}) };
67
+ return hostOpener(HOSTED_SITE_PAGE_ACTION, params);
68
+ }
@@ -1,8 +1,12 @@
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.
1
+ // Hosted checkout. A Branded Lite hosted in a host shell (Spaces) pays as the member the host
2
+ // lent it: Headless `createRedirectSession` does not take a Member instance, so the HOST opens the site's
3
+ // checkout signed in as that member. The worker names only its ecom checkout; the host reads Wix's
4
+ // checkout URL, mints a one-time member session and opens the site's `createSessionCookie` → checkout.
5
+ // The session token never reaches this worker.
6
+ //
7
+ // Classic Wix Events tickets and classic Pricing Plans have no ecom checkout: for those the
8
+ // worker names the page by id (`himi.host.memberSitePage`) and the host opens the site's own event or
9
+ // plans page the same way, signed in — where the member picks and pays on the site.
6
10
 
7
11
  import { dispatch } from "@himalaya/dispatch";
8
12
  import { hostSiteFromInput } from "./branded-lite-hosted.js";
@@ -13,6 +17,12 @@ import { hostSiteFromInput } from "./branded-lite-hosted.js";
13
17
  * URL (which carries the member's session token) stays in the host; only the ticket crosses here. */
14
18
  export const HOSTED_CHECKOUT_ACTION = "himi.host.memberCheckout";
15
19
  export const HOSTED_CHECKOUT_OPEN_ACTION = "himi.host.memberCheckout.open";
20
+ /** Prepares a site PAGE (not an ecom checkout) the same way; opened with [HOSTED_CHECKOUT_OPEN_ACTION]. */
21
+ export const HOSTED_SITE_PAGE_ACTION = "himi.host.memberSitePage";
22
+
23
+ /** A site page a hosted worker may ask its host for — ids only, never a URL: the host reads the URL
24
+ * from Wix itself and decides whether the member may be signed in there. */
25
+ export type HostedSitePage = { page: "event"; eventId: string } | { page: "plans"; planId?: string };
16
26
 
17
27
  /** Whether this worker runs inside a host that opened a site for it. */
18
28
  export function isHostedWorker(): boolean {
@@ -20,14 +30,14 @@ export function isHostedWorker(): boolean {
20
30
  }
21
31
 
22
32
  type HostAnswer = { opened?: unknown; ticket?: unknown; payload?: { opened?: unknown; ticket?: unknown } } | null;
33
+ type HostOpener = (stillMayCharge: () => Promise<boolean>) => Promise<boolean | "withdrawn">;
23
34
 
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"> {
35
+ /** Prepare with [action] [params] → re-read the guard → open by ticket. A host without the verbs, or one
36
+ * that refused (Wix named no page, an unsafe URL, no account session), → false, never a throw. */
37
+ function hostOpener(action: string, params: Record<string, string>): HostOpener {
28
38
  return async (stillMayCharge) => {
29
39
  try {
30
- const prepared = await dispatch(HOSTED_CHECKOUT_ACTION, { checkoutId }) as HostAnswer;
40
+ const prepared = await dispatch(action, params) as HostAnswer;
31
41
  const ticket = prepared?.ticket ?? prepared?.payload?.ticket;
32
42
  if (typeof ticket !== "string" || !ticket) return false;
33
43
  // An unopened ticket simply expires in the host: nothing to cancel.
@@ -40,3 +50,19 @@ export function hostedCheckoutOpener(checkoutId: string): (stillMayCharge: () =>
40
50
  }
41
51
  };
42
52
  }
53
+
54
+ /** `payCashierOrder`'s `openCashier` for a hosted app: the host prepares [checkoutId]'s checkout as the
55
+ * lent member, the caller's guard is re-read, and only then does the host open it. Resolves whether it
56
+ * opened ("withdrawn" when the guard said no); a host without the verbs (or that refused) → false. */
57
+ export function hostedCheckoutOpener(checkoutId: string): HostOpener {
58
+ return hostOpener(HOSTED_CHECKOUT_ACTION, { checkoutId });
59
+ }
60
+
61
+ /** The site's own [page] opened by the host, signed in as the lent member where the host deems it safe
62
+ * (a visitor otherwise) — the same two steps and answers as [hostedCheckoutOpener]. */
63
+ export function hostedSitePageOpener(page: HostedSitePage): HostOpener {
64
+ const params: Record<string, string> = page.page === "event"
65
+ ? { page: "event", eventId: page.eventId }
66
+ : { page: "plans", ...(page.planId ? { planId: page.planId } : {}) };
67
+ return hostOpener(HOSTED_SITE_PAGE_ACTION, params);
68
+ }
@@ -35,7 +35,10 @@ import {
35
35
  type EventRow,
36
36
  } from "../_shared/himi-events";
37
37
  import { initials } from "../_shared/himi-text.js";
38
- import { isHostedWorker } from "../_shared/hosted-checkout.js";
38
+ import { hostedSitePageOpener, isHostedWorker } from "../_shared/hosted-checkout.js";
39
+
40
+ /** A hosted signed-in event-page open is in flight (see openRegistration). */
41
+ let hostedOpenInFlight = false;
39
42
  import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
40
43
  import { openRichLink } from "../_shared/rich-content-link.js";
41
44
  import { richBody } from "../_shared/rich-body.js";
@@ -579,7 +582,8 @@ const svc = defineService<S, {
579
582
  // `checkoutReservation` needs a buyer + guest form this screen does not collect, and the
580
583
  // redirect session needs a headless OAuth client a hosted app does not have. Skip both —
581
584
  // the native chain would also leave a ticket reservation held for nothing — and open the
582
- // event's own page, where the member registers on the site.
585
+ // event's own page, where the member registers on the site: signed in by the host where it can
586
+ // (`himi.host.memberSitePage`), else as a visitor.
583
587
  if (s.registrationKind === "tickets" && s.id && !isHostedWorker()) {
584
588
  let reservationId = "";
585
589
  try {
@@ -658,6 +662,31 @@ const svc = defineService<S, {
658
662
  // fall through to registrationUrl
659
663
  }
660
664
  }
665
+ // Hosted: the HOST opens the event's own page signed in as the lent member — ids only; it
666
+ // reads the page URL from Wix and refuses (a custom domain → a visitor page; no published Events
667
+ // page → nothing) on its own. Nothing here can change between its prepare and its open (the event
668
+ // is fixed and the page is where the member chooses and pays), so the guard has nothing to re-read.
669
+ // A host that refused or failed leaves the visitor page below.
670
+ if (isHostedWorker() && s.id && (s.registrationKind === "tickets" || s.registrationKind === "rsvp")) {
671
+ // One signed-in open at a time: the host keeps a single one-time ticket, so a second tap would
672
+ // replace the first tap's ticket and drop that tap into the signed-OUT visitor page below.
673
+ if (hostedOpenInFlight) return s;
674
+ hostedOpenInFlight = true;
675
+ let opened: boolean | "withdrawn";
676
+ try {
677
+ opened = await hostedSitePageOpener({ page: "event", eventId: s.id })(async () => true);
678
+ } finally {
679
+ hostedOpenInFlight = false;
680
+ }
681
+ if (opened === true) {
682
+ return derive({
683
+ ...s,
684
+ actionMessage: s.registrationKind === "tickets"
685
+ ? copyParam("event.checkoutOpened", "Complete checkout in the browser to get your tickets.")
686
+ : "",
687
+ });
688
+ }
689
+ }
661
690
  if (!s.registrationUrl) {
662
691
  return derive({ ...s, actionMessage: copyParam("event.registrationOnPage", "Registration for this event happens on the event page.") });
663
692
  }
@@ -1,8 +1,12 @@
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.
1
+ // Hosted checkout. A Branded Lite hosted in a host shell (Spaces) pays as the member the host
2
+ // lent it: Headless `createRedirectSession` does not take a Member instance, so the HOST opens the site's
3
+ // checkout signed in as that member. The worker names only its ecom checkout; the host reads Wix's
4
+ // checkout URL, mints a one-time member session and opens the site's `createSessionCookie` → checkout.
5
+ // The session token never reaches this worker.
6
+ //
7
+ // Classic Wix Events tickets and classic Pricing Plans have no ecom checkout: for those the
8
+ // worker names the page by id (`himi.host.memberSitePage`) and the host opens the site's own event or
9
+ // plans page the same way, signed in — where the member picks and pays on the site.
6
10
 
7
11
  import { dispatch } from "@himalaya/dispatch";
8
12
  import { hostSiteFromInput } from "./branded-lite-hosted.js";
@@ -13,6 +17,12 @@ import { hostSiteFromInput } from "./branded-lite-hosted.js";
13
17
  * URL (which carries the member's session token) stays in the host; only the ticket crosses here. */
14
18
  export const HOSTED_CHECKOUT_ACTION = "himi.host.memberCheckout";
15
19
  export const HOSTED_CHECKOUT_OPEN_ACTION = "himi.host.memberCheckout.open";
20
+ /** Prepares a site PAGE (not an ecom checkout) the same way; opened with [HOSTED_CHECKOUT_OPEN_ACTION]. */
21
+ export const HOSTED_SITE_PAGE_ACTION = "himi.host.memberSitePage";
22
+
23
+ /** A site page a hosted worker may ask its host for — ids only, never a URL: the host reads the URL
24
+ * from Wix itself and decides whether the member may be signed in there. */
25
+ export type HostedSitePage = { page: "event"; eventId: string } | { page: "plans"; planId?: string };
16
26
 
17
27
  /** Whether this worker runs inside a host that opened a site for it. */
18
28
  export function isHostedWorker(): boolean {
@@ -20,14 +30,14 @@ export function isHostedWorker(): boolean {
20
30
  }
21
31
 
22
32
  type HostAnswer = { opened?: unknown; ticket?: unknown; payload?: { opened?: unknown; ticket?: unknown } } | null;
33
+ type HostOpener = (stillMayCharge: () => Promise<boolean>) => Promise<boolean | "withdrawn">;
23
34
 
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"> {
35
+ /** Prepare with [action] [params] → re-read the guard → open by ticket. A host without the verbs, or one
36
+ * that refused (Wix named no page, an unsafe URL, no account session), → false, never a throw. */
37
+ function hostOpener(action: string, params: Record<string, string>): HostOpener {
28
38
  return async (stillMayCharge) => {
29
39
  try {
30
- const prepared = await dispatch(HOSTED_CHECKOUT_ACTION, { checkoutId }) as HostAnswer;
40
+ const prepared = await dispatch(action, params) as HostAnswer;
31
41
  const ticket = prepared?.ticket ?? prepared?.payload?.ticket;
32
42
  if (typeof ticket !== "string" || !ticket) return false;
33
43
  // An unopened ticket simply expires in the host: nothing to cancel.
@@ -40,3 +50,19 @@ export function hostedCheckoutOpener(checkoutId: string): (stillMayCharge: () =>
40
50
  }
41
51
  };
42
52
  }
53
+
54
+ /** `payCashierOrder`'s `openCashier` for a hosted app: the host prepares [checkoutId]'s checkout as the
55
+ * lent member, the caller's guard is re-read, and only then does the host open it. Resolves whether it
56
+ * opened ("withdrawn" when the guard said no); a host without the verbs (or that refused) → false. */
57
+ export function hostedCheckoutOpener(checkoutId: string): HostOpener {
58
+ return hostOpener(HOSTED_CHECKOUT_ACTION, { checkoutId });
59
+ }
60
+
61
+ /** The site's own [page] opened by the host, signed in as the lent member where the host deems it safe
62
+ * (a visitor otherwise) — the same two steps and answers as [hostedCheckoutOpener]. */
63
+ export function hostedSitePageOpener(page: HostedSitePage): HostOpener {
64
+ const params: Record<string, string> = page.page === "event"
65
+ ? { page: "event", eventId: page.eventId }
66
+ : { page: "plans", ...(page.planId ? { planId: page.planId } : {}) };
67
+ return hostOpener(HOSTED_SITE_PAGE_ACTION, params);
68
+ }
@@ -35,7 +35,10 @@ import {
35
35
  type EventRow,
36
36
  } from "../_shared/himi-events";
37
37
  import { initials } from "../_shared/himi-text.js";
38
- import { isHostedWorker } from "../_shared/hosted-checkout.js";
38
+ import { hostedSitePageOpener, isHostedWorker } from "../_shared/hosted-checkout.js";
39
+
40
+ /** A hosted signed-in event-page open is in flight (see openRegistration). */
41
+ let hostedOpenInFlight = false;
39
42
  import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
40
43
  import { openRichLink } from "../_shared/rich-content-link.js";
41
44
  import { richBody } from "../_shared/rich-body.js";
@@ -579,7 +582,8 @@ const svc = defineService<S, {
579
582
  // `checkoutReservation` needs a buyer + guest form this screen does not collect, and the
580
583
  // redirect session needs a headless OAuth client a hosted app does not have. Skip both —
581
584
  // the native chain would also leave a ticket reservation held for nothing — and open the
582
- // event's own page, where the member registers on the site.
585
+ // event's own page, where the member registers on the site: signed in by the host where it can
586
+ // (`himi.host.memberSitePage`), else as a visitor.
583
587
  if (s.registrationKind === "tickets" && s.id && !isHostedWorker()) {
584
588
  let reservationId = "";
585
589
  try {
@@ -658,6 +662,31 @@ const svc = defineService<S, {
658
662
  // fall through to registrationUrl
659
663
  }
660
664
  }
665
+ // Hosted: the HOST opens the event's own page signed in as the lent member — ids only; it
666
+ // reads the page URL from Wix and refuses (a custom domain → a visitor page; no published Events
667
+ // page → nothing) on its own. Nothing here can change between its prepare and its open (the event
668
+ // is fixed and the page is where the member chooses and pays), so the guard has nothing to re-read.
669
+ // A host that refused or failed leaves the visitor page below.
670
+ if (isHostedWorker() && s.id && (s.registrationKind === "tickets" || s.registrationKind === "rsvp")) {
671
+ // One signed-in open at a time: the host keeps a single one-time ticket, so a second tap would
672
+ // replace the first tap's ticket and drop that tap into the signed-OUT visitor page below.
673
+ if (hostedOpenInFlight) return s;
674
+ hostedOpenInFlight = true;
675
+ let opened: boolean | "withdrawn";
676
+ try {
677
+ opened = await hostedSitePageOpener({ page: "event", eventId: s.id })(async () => true);
678
+ } finally {
679
+ hostedOpenInFlight = false;
680
+ }
681
+ if (opened === true) {
682
+ return derive({
683
+ ...s,
684
+ actionMessage: s.registrationKind === "tickets"
685
+ ? copyParam("event.checkoutOpened", "Complete checkout in the browser to get your tickets.")
686
+ : "",
687
+ });
688
+ }
689
+ }
661
690
  if (!s.registrationUrl) {
662
691
  return derive({ ...s, actionMessage: copyParam("event.registrationOnPage", "Registration for this event happens on the event page.") });
663
692
  }
@@ -1,8 +1,12 @@
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.
1
+ // Hosted checkout. A Branded Lite hosted in a host shell (Spaces) pays as the member the host
2
+ // lent it: Headless `createRedirectSession` does not take a Member instance, so the HOST opens the site's
3
+ // checkout signed in as that member. The worker names only its ecom checkout; the host reads Wix's
4
+ // checkout URL, mints a one-time member session and opens the site's `createSessionCookie` → checkout.
5
+ // The session token never reaches this worker.
6
+ //
7
+ // Classic Wix Events tickets and classic Pricing Plans have no ecom checkout: for those the
8
+ // worker names the page by id (`himi.host.memberSitePage`) and the host opens the site's own event or
9
+ // plans page the same way, signed in — where the member picks and pays on the site.
6
10
 
7
11
  import { dispatch } from "@himalaya/dispatch";
8
12
  import { hostSiteFromInput } from "./branded-lite-hosted.js";
@@ -13,6 +17,12 @@ import { hostSiteFromInput } from "./branded-lite-hosted.js";
13
17
  * URL (which carries the member's session token) stays in the host; only the ticket crosses here. */
14
18
  export const HOSTED_CHECKOUT_ACTION = "himi.host.memberCheckout";
15
19
  export const HOSTED_CHECKOUT_OPEN_ACTION = "himi.host.memberCheckout.open";
20
+ /** Prepares a site PAGE (not an ecom checkout) the same way; opened with [HOSTED_CHECKOUT_OPEN_ACTION]. */
21
+ export const HOSTED_SITE_PAGE_ACTION = "himi.host.memberSitePage";
22
+
23
+ /** A site page a hosted worker may ask its host for — ids only, never a URL: the host reads the URL
24
+ * from Wix itself and decides whether the member may be signed in there. */
25
+ export type HostedSitePage = { page: "event"; eventId: string } | { page: "plans"; planId?: string };
16
26
 
17
27
  /** Whether this worker runs inside a host that opened a site for it. */
18
28
  export function isHostedWorker(): boolean {
@@ -20,14 +30,14 @@ export function isHostedWorker(): boolean {
20
30
  }
21
31
 
22
32
  type HostAnswer = { opened?: unknown; ticket?: unknown; payload?: { opened?: unknown; ticket?: unknown } } | null;
33
+ type HostOpener = (stillMayCharge: () => Promise<boolean>) => Promise<boolean | "withdrawn">;
23
34
 
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"> {
35
+ /** Prepare with [action] [params] → re-read the guard → open by ticket. A host without the verbs, or one
36
+ * that refused (Wix named no page, an unsafe URL, no account session), → false, never a throw. */
37
+ function hostOpener(action: string, params: Record<string, string>): HostOpener {
28
38
  return async (stillMayCharge) => {
29
39
  try {
30
- const prepared = await dispatch(HOSTED_CHECKOUT_ACTION, { checkoutId }) as HostAnswer;
40
+ const prepared = await dispatch(action, params) as HostAnswer;
31
41
  const ticket = prepared?.ticket ?? prepared?.payload?.ticket;
32
42
  if (typeof ticket !== "string" || !ticket) return false;
33
43
  // An unopened ticket simply expires in the host: nothing to cancel.
@@ -40,3 +50,19 @@ export function hostedCheckoutOpener(checkoutId: string): (stillMayCharge: () =>
40
50
  }
41
51
  };
42
52
  }
53
+
54
+ /** `payCashierOrder`'s `openCashier` for a hosted app: the host prepares [checkoutId]'s checkout as the
55
+ * lent member, the caller's guard is re-read, and only then does the host open it. Resolves whether it
56
+ * opened ("withdrawn" when the guard said no); a host without the verbs (or that refused) → false. */
57
+ export function hostedCheckoutOpener(checkoutId: string): HostOpener {
58
+ return hostOpener(HOSTED_CHECKOUT_ACTION, { checkoutId });
59
+ }
60
+
61
+ /** The site's own [page] opened by the host, signed in as the lent member where the host deems it safe
62
+ * (a visitor otherwise) — the same two steps and answers as [hostedCheckoutOpener]. */
63
+ export function hostedSitePageOpener(page: HostedSitePage): HostOpener {
64
+ const params: Record<string, string> = page.page === "event"
65
+ ? { page: "event", eventId: page.eventId }
66
+ : { page: "plans", ...(page.planId ? { planId: page.planId } : {}) };
67
+ return hostOpener(HOSTED_SITE_PAGE_ACTION, params);
68
+ }
@@ -35,7 +35,10 @@ import {
35
35
  type EventRow,
36
36
  } from "../_shared/himi-events";
37
37
  import { initials } from "../_shared/himi-text.js";
38
- import { isHostedWorker } from "../_shared/hosted-checkout.js";
38
+ import { hostedSitePageOpener, isHostedWorker } from "../_shared/hosted-checkout.js";
39
+
40
+ /** A hosted signed-in event-page open is in flight (see openRegistration). */
41
+ let hostedOpenInFlight = false;
39
42
  import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
40
43
  import { openRichLink } from "../_shared/rich-content-link.js";
41
44
  import { richBody } from "../_shared/rich-body.js";
@@ -579,7 +582,8 @@ const svc = defineService<S, {
579
582
  // `checkoutReservation` needs a buyer + guest form this screen does not collect, and the
580
583
  // redirect session needs a headless OAuth client a hosted app does not have. Skip both —
581
584
  // the native chain would also leave a ticket reservation held for nothing — and open the
582
- // event's own page, where the member registers on the site.
585
+ // event's own page, where the member registers on the site: signed in by the host where it can
586
+ // (`himi.host.memberSitePage`), else as a visitor.
583
587
  if (s.registrationKind === "tickets" && s.id && !isHostedWorker()) {
584
588
  let reservationId = "";
585
589
  try {
@@ -658,6 +662,31 @@ const svc = defineService<S, {
658
662
  // fall through to registrationUrl
659
663
  }
660
664
  }
665
+ // Hosted: the HOST opens the event's own page signed in as the lent member — ids only; it
666
+ // reads the page URL from Wix and refuses (a custom domain → a visitor page; no published Events
667
+ // page → nothing) on its own. Nothing here can change between its prepare and its open (the event
668
+ // is fixed and the page is where the member chooses and pays), so the guard has nothing to re-read.
669
+ // A host that refused or failed leaves the visitor page below.
670
+ if (isHostedWorker() && s.id && (s.registrationKind === "tickets" || s.registrationKind === "rsvp")) {
671
+ // One signed-in open at a time: the host keeps a single one-time ticket, so a second tap would
672
+ // replace the first tap's ticket and drop that tap into the signed-OUT visitor page below.
673
+ if (hostedOpenInFlight) return s;
674
+ hostedOpenInFlight = true;
675
+ let opened: boolean | "withdrawn";
676
+ try {
677
+ opened = await hostedSitePageOpener({ page: "event", eventId: s.id })(async () => true);
678
+ } finally {
679
+ hostedOpenInFlight = false;
680
+ }
681
+ if (opened === true) {
682
+ return derive({
683
+ ...s,
684
+ actionMessage: s.registrationKind === "tickets"
685
+ ? copyParam("event.checkoutOpened", "Complete checkout in the browser to get your tickets.")
686
+ : "",
687
+ });
688
+ }
689
+ }
661
690
  if (!s.registrationUrl) {
662
691
  return derive({ ...s, actionMessage: copyParam("event.registrationOnPage", "Registration for this event happens on the event page.") });
663
692
  }
@@ -1,8 +1,12 @@
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.
1
+ // Hosted checkout. A Branded Lite hosted in a host shell (Spaces) pays as the member the host
2
+ // lent it: Headless `createRedirectSession` does not take a Member instance, so the HOST opens the site's
3
+ // checkout signed in as that member. The worker names only its ecom checkout; the host reads Wix's
4
+ // checkout URL, mints a one-time member session and opens the site's `createSessionCookie` → checkout.
5
+ // The session token never reaches this worker.
6
+ //
7
+ // Classic Wix Events tickets and classic Pricing Plans have no ecom checkout: for those the
8
+ // worker names the page by id (`himi.host.memberSitePage`) and the host opens the site's own event or
9
+ // plans page the same way, signed in — where the member picks and pays on the site.
6
10
 
7
11
  import { dispatch } from "@himalaya/dispatch";
8
12
  import { hostSiteFromInput } from "./branded-lite-hosted.js";
@@ -13,6 +17,12 @@ import { hostSiteFromInput } from "./branded-lite-hosted.js";
13
17
  * URL (which carries the member's session token) stays in the host; only the ticket crosses here. */
14
18
  export const HOSTED_CHECKOUT_ACTION = "himi.host.memberCheckout";
15
19
  export const HOSTED_CHECKOUT_OPEN_ACTION = "himi.host.memberCheckout.open";
20
+ /** Prepares a site PAGE (not an ecom checkout) the same way; opened with [HOSTED_CHECKOUT_OPEN_ACTION]. */
21
+ export const HOSTED_SITE_PAGE_ACTION = "himi.host.memberSitePage";
22
+
23
+ /** A site page a hosted worker may ask its host for — ids only, never a URL: the host reads the URL
24
+ * from Wix itself and decides whether the member may be signed in there. */
25
+ export type HostedSitePage = { page: "event"; eventId: string } | { page: "plans"; planId?: string };
16
26
 
17
27
  /** Whether this worker runs inside a host that opened a site for it. */
18
28
  export function isHostedWorker(): boolean {
@@ -20,14 +30,14 @@ export function isHostedWorker(): boolean {
20
30
  }
21
31
 
22
32
  type HostAnswer = { opened?: unknown; ticket?: unknown; payload?: { opened?: unknown; ticket?: unknown } } | null;
33
+ type HostOpener = (stillMayCharge: () => Promise<boolean>) => Promise<boolean | "withdrawn">;
23
34
 
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"> {
35
+ /** Prepare with [action] [params] → re-read the guard → open by ticket. A host without the verbs, or one
36
+ * that refused (Wix named no page, an unsafe URL, no account session), → false, never a throw. */
37
+ function hostOpener(action: string, params: Record<string, string>): HostOpener {
28
38
  return async (stillMayCharge) => {
29
39
  try {
30
- const prepared = await dispatch(HOSTED_CHECKOUT_ACTION, { checkoutId }) as HostAnswer;
40
+ const prepared = await dispatch(action, params) as HostAnswer;
31
41
  const ticket = prepared?.ticket ?? prepared?.payload?.ticket;
32
42
  if (typeof ticket !== "string" || !ticket) return false;
33
43
  // An unopened ticket simply expires in the host: nothing to cancel.
@@ -40,3 +50,19 @@ export function hostedCheckoutOpener(checkoutId: string): (stillMayCharge: () =>
40
50
  }
41
51
  };
42
52
  }
53
+
54
+ /** `payCashierOrder`'s `openCashier` for a hosted app: the host prepares [checkoutId]'s checkout as the
55
+ * lent member, the caller's guard is re-read, and only then does the host open it. Resolves whether it
56
+ * opened ("withdrawn" when the guard said no); a host without the verbs (or that refused) → false. */
57
+ export function hostedCheckoutOpener(checkoutId: string): HostOpener {
58
+ return hostOpener(HOSTED_CHECKOUT_ACTION, { checkoutId });
59
+ }
60
+
61
+ /** The site's own [page] opened by the host, signed in as the lent member where the host deems it safe
62
+ * (a visitor otherwise) — the same two steps and answers as [hostedCheckoutOpener]. */
63
+ export function hostedSitePageOpener(page: HostedSitePage): HostOpener {
64
+ const params: Record<string, string> = page.page === "event"
65
+ ? { page: "event", eventId: page.eventId }
66
+ : { page: "plans", ...(page.planId ? { planId: page.planId } : {}) };
67
+ return hostOpener(HOSTED_SITE_PAGE_ACTION, params);
68
+ }
@@ -35,7 +35,10 @@ import {
35
35
  type EventRow,
36
36
  } from "../_shared/himi-events";
37
37
  import { initials } from "../_shared/himi-text.js";
38
- import { isHostedWorker } from "../_shared/hosted-checkout.js";
38
+ import { hostedSitePageOpener, isHostedWorker } from "../_shared/hosted-checkout.js";
39
+
40
+ /** A hosted signed-in event-page open is in flight (see openRegistration). */
41
+ let hostedOpenInFlight = false;
39
42
  import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
40
43
  import { openRichLink } from "../_shared/rich-content-link.js";
41
44
  import { richBody } from "../_shared/rich-body.js";
@@ -579,7 +582,8 @@ const svc = defineService<S, {
579
582
  // `checkoutReservation` needs a buyer + guest form this screen does not collect, and the
580
583
  // redirect session needs a headless OAuth client a hosted app does not have. Skip both —
581
584
  // the native chain would also leave a ticket reservation held for nothing — and open the
582
- // event's own page, where the member registers on the site.
585
+ // event's own page, where the member registers on the site: signed in by the host where it can
586
+ // (`himi.host.memberSitePage`), else as a visitor.
583
587
  if (s.registrationKind === "tickets" && s.id && !isHostedWorker()) {
584
588
  let reservationId = "";
585
589
  try {
@@ -658,6 +662,31 @@ const svc = defineService<S, {
658
662
  // fall through to registrationUrl
659
663
  }
660
664
  }
665
+ // Hosted: the HOST opens the event's own page signed in as the lent member — ids only; it
666
+ // reads the page URL from Wix and refuses (a custom domain → a visitor page; no published Events
667
+ // page → nothing) on its own. Nothing here can change between its prepare and its open (the event
668
+ // is fixed and the page is where the member chooses and pays), so the guard has nothing to re-read.
669
+ // A host that refused or failed leaves the visitor page below.
670
+ if (isHostedWorker() && s.id && (s.registrationKind === "tickets" || s.registrationKind === "rsvp")) {
671
+ // One signed-in open at a time: the host keeps a single one-time ticket, so a second tap would
672
+ // replace the first tap's ticket and drop that tap into the signed-OUT visitor page below.
673
+ if (hostedOpenInFlight) return s;
674
+ hostedOpenInFlight = true;
675
+ let opened: boolean | "withdrawn";
676
+ try {
677
+ opened = await hostedSitePageOpener({ page: "event", eventId: s.id })(async () => true);
678
+ } finally {
679
+ hostedOpenInFlight = false;
680
+ }
681
+ if (opened === true) {
682
+ return derive({
683
+ ...s,
684
+ actionMessage: s.registrationKind === "tickets"
685
+ ? copyParam("event.checkoutOpened", "Complete checkout in the browser to get your tickets.")
686
+ : "",
687
+ });
688
+ }
689
+ }
661
690
  if (!s.registrationUrl) {
662
691
  return derive({ ...s, actionMessage: copyParam("event.registrationOnPage", "Registration for this event happens on the event page.") });
663
692
  }