@wix/himalaya-cli 0.1558.0 → 0.1560.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.
@@ -164,7 +164,7 @@ export declare function queryParticipants(programId: string, opts?: {
164
164
  limit?: number;
165
165
  }, readOptions?: WixCallOptions): Promise<WixQueryParticipantsResponse>;
166
166
  /** Participant lifecycle state. */
167
- export type WixParticipantState = "JOINED" | "PAYMENT_PENDING" | "APPROVAL_PENDING" | "COMPLETED" | "INVITED" | "REMOVED" | string;
167
+ export type WixParticipantState = "JOINED" | "PAYMENT_PENDING" | "ENROLLMENT_PENDING" | "APPROVAL_PENDING" | "COMPLETED" | "INVITED" | "REMOVED" | string;
168
168
  export interface WixParticipantPaymentPending {
169
169
  /** The payable handle for the cashier rail, when the program charges. */
170
170
  purchaseFlowId?: string;
@@ -172,7 +172,7 @@ export interface WixParticipantPaymentPending {
172
172
  snapshotId?: string;
173
173
  }
174
174
  /**
175
- * The participant JoinProgram hands back — a DIFFERENT entity from the roster
175
+ * The compatibility result joinProgram returns — a DIFFERENT entity from the roster
176
176
  * `WixParticipant` above, which is why it gets its own name. This one describes the join
177
177
  * outcome (`state`, `paymentPendingOptions`); the roster one describes who the member is
178
178
  * and how far they have got (`member`, `progress`, `enrollmentStatus`).
@@ -195,7 +195,16 @@ export interface WixJoinedParticipant {
195
195
  export interface WixJoinProgramResponse {
196
196
  participant?: WixJoinedParticipant;
197
197
  }
198
- /** Join a program as the signed-in member. Branch on `participant.state`. */
199
- export declare function joinProgram(programId: string): Promise<WixJoinProgramResponse>;
200
- /** The cashier handle to settle, or "" when the join needs no payment. */
198
+ /**
199
+ * Join as the signed-in member through Join Applications. Read the current
200
+ * application first so an existing application is resumed without another create.
201
+ * Approved paid applications prepare
202
+ * SINGLE_PAYMENT and return the Wix Payments order as paymentPendingOptions.orderId.
203
+ * mayPreparePayment rechecks the caller's checkout guard before creating that order.
204
+ * @scope member
205
+ */
206
+ export declare function joinProgram(programId: string, options?: {
207
+ mayPreparePayment?: () => boolean;
208
+ }): Promise<WixJoinProgramResponse>;
209
+ /** The cashier handle to settle, or "" when no payable order is available. */
201
210
  export declare function payableIdOf(participant?: WixJoinedParticipant): string;
@@ -29,13 +29,41 @@ function queryParticipants(programId, opts, readOptions) {
29
29
  };
30
30
  return wixCall("POST", "/online-programs/v3/participants/query", { query, fields: ["PROGRESS"] }, readOptions);
31
31
  }
32
- function joinProgram(programId) {
33
- if (!programId) throw new Error("joinProgram: programId is required");
34
- return wixCall(
35
- "POST",
36
- `/online-programs/v3/participants/${encodeURIComponent(programId)}/join`,
37
- {}
38
- );
32
+ function joinProgramError(code, message) {
33
+ return Object.assign(new Error(`joinProgram: ${message}`), { code });
34
+ }
35
+ var JOIN_APPLICATIONS = "/online-programs/participants/v3/join-applications";
36
+ async function joinProgram(programId, options) {
37
+ if (!programId) throw joinProgramError("program_id_required", "programId is required");
38
+ let response;
39
+ let currentReadSucceeded = false;
40
+ try {
41
+ response = await wixCall("GET", `${JOIN_APPLICATIONS}/current/${encodeURIComponent(programId)}`);
42
+ currentReadSucceeded = true;
43
+ } catch {
44
+ }
45
+ if (!currentReadSucceeded) {
46
+ response = await wixCall("POST", JOIN_APPLICATIONS, { joinApplication: { programId } });
47
+ }
48
+ let application = response?.joinApplication;
49
+ if (!application?.id || application.programId !== programId) throw joinProgramError("invalid_join_application", "invalid join application");
50
+ if (application.status === "APPROVED" && ["PAYMENT_PENDING", "PAYMENT_FAILED", "PAYMENT_CANCELED"].includes(application.paymentStatus ?? "")) {
51
+ if (options?.mayPreparePayment && !options.mayPreparePayment()) throw joinProgramError("payment_preparation_withdrawn", "payment preparation withdrawn");
52
+ const prepared = await wixCall("POST", `${JOIN_APPLICATIONS}/${encodeURIComponent(application.id)}/prepare-payment`, { paymentType: "SINGLE_PAYMENT" });
53
+ if (prepared?.joinApplication?.id !== application.id || prepared.joinApplication.programId !== programId) throw joinProgramError("invalid_prepared_application", "invalid prepared application");
54
+ application = prepared.joinApplication;
55
+ }
56
+ const participant = { programId, memberId: application.memberId };
57
+ if (application.status === "APPROVAL_PENDING") return { participant: { ...participant, state: "APPROVAL_PENDING" } };
58
+ if (application.status !== "APPROVED") throw joinProgramError("join_application_not_approved", "application not approved");
59
+ if (application.paymentStatus === "PENDING_OWNER_APPROVAL") return { participant: { ...participant, state: "ENROLLMENT_PENDING" } };
60
+ if (["NO_PAYMENT_REQUIRED", "PAYMENT_SUCCESSFUL"].includes(application.paymentStatus ?? "")) {
61
+ return { participant: application.participantId ? { ...participant, id: application.participantId, state: "JOINED" } : { ...participant, state: "ENROLLMENT_PENDING" } };
62
+ }
63
+ if (application.paymentStatus === "PAYMENT_IN_PROGRESS" && application.paymentInfo?.paymentOrderId) {
64
+ return { participant: { ...participant, state: "PAYMENT_PENDING", paymentPendingOptions: { orderId: application.paymentInfo.paymentOrderId } } };
65
+ }
66
+ throw joinProgramError("join_application_not_ready", "enrollment or payment is not ready");
39
67
  }
40
68
  function payableIdOf(participant) {
41
69
  const pending = participant?.paymentPendingOptions;
package/dist/cli.mjs CHANGED
@@ -23086,9 +23086,9 @@ function inferScreenDisplayName(screenId) {
23086
23086
  const suffix = id.slice(product2[0].length).replace(/^_/, "");
23087
23087
  return `${kind}Product Details${suffix ? ` (${suffix})` : ""}`;
23088
23088
  }
23089
- const opaque = /(?:_|\/)([0-9a-f]{8}-[0-9a-f-]{27,}|[0-9]+|[0-9a-f]{16,})$/i.exec(id);
23090
- const words = (opaque ? id.slice(0, opaque.index) : id).replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[_\/\s-]+/).filter(Boolean).map((word) => /^(cms|faq|api|ai|id|url)$/i.test(word) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
23091
- return `${words || screenId}${opaque ? ` (${opaque[1]})` : ""}`;
23089
+ const opaque2 = /(?:_|\/)([0-9a-f]{8}-[0-9a-f-]{27,}|[0-9]+|[0-9a-f]{16,})$/i.exec(id);
23090
+ const words = (opaque2 ? id.slice(0, opaque2.index) : id).replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[_\/\s-]+/).filter(Boolean).map((word) => /^(cms|faq|api|ai|id|url)$/i.test(word) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
23091
+ return `${words || screenId}${opaque2 ? ` (${opaque2[1]})` : ""}`;
23092
23092
  }
23093
23093
  function screenDisplayName(screenId, displayName3, title, navigationLabel) {
23094
23094
  const literal = (value2) => typeof value2 === "string" && !!value2.trim() && !value2.trim().startsWith("$") && !/^__[A-Z0-9_]+__$/.test(value2.trim());
@@ -143328,7 +143328,7 @@ Transport: same targets/tokens as himi-serve \u2014 --target <local|prod|bo>, --
143328
143328
  --telemetry-url, --control-token/--telemetry-token, or HIMI_* env.
143329
143329
  Guides: ${DEVELOPER_PORTAL_URL}`;
143330
143330
  COMMANDS = [
143331
- { name: "one-app", summary: "collect One App source configuration and evidence for reconstruction; validate-topbar checks authored settings and resolved platform bars; validate-design compares the source theme's colors and button shape with the migrated tokens.json; coverage inventories and verifies source requirements. Branded falls back to Free/Spaces only when absent. Collection does not launch a model or build the app. User-scoped Wix authentication is required; WIX_API_KEY is not used.", required: "migrate --msid <uuid> [--app-instance branded|free] [--prompt <text> | --prompt-file <file>] [--name <slug>] [--dir <parent>] [--target bo] [--top-tabs bottom-tabs|keep] [--json] | validate-topbar --evidence-dir <.himi/one-app> --content <app-dir> [--json] | validate-design --evidence-dir <.himi/one-app> --content <app-dir> [--json] [--fix] [--record --release-hash <sha256>] | coverage init|sync|check|verify --evidence-dir <.himi/one-app> --content <app-dir> [--release-hash <sha256>]" },
143331
+ { name: "one-app", summary: "collect One App source configuration and evidence for reconstruction; validate-topbar checks authored settings and resolved platform bars; validate-design compares the source theme's colors, heading/body font families, button shape and accent text contrast with the migrated tokens.json; coverage inventories and verifies source requirements. Branded falls back to Free/Spaces only when absent. Collection does not launch a model or build the app. User-scoped Wix authentication is required; WIX_API_KEY is not used.", required: "migrate --msid <uuid> [--app-instance branded|free] [--prompt <text> | --prompt-file <file>] [--name <slug>] [--dir <parent>] [--target bo] [--top-tabs bottom-tabs|keep] [--json] | validate-topbar --evidence-dir <.himi/one-app> --content <app-dir> [--json] | validate-design --evidence-dir <.himi/one-app> --content <app-dir> [--json] [--fix] [--record --release-hash <sha256>] | coverage init|sync|check|verify --evidence-dir <.himi/one-app> --content <app-dir> [--release-hash <sha256>]" },
143332
143332
  { name: "hhp", summary: "author responsive HTML/CSS/TypeScript screens compiled to native SDUI and T5 workers; scaffold, inspect the app profile, lint or compile declared sources", required: "init <screen> | profile | lint | compile [--app <name>|--content <dir>] [--json] [--dry-run]" },
143333
143333
  { name: "captcha", summary: "create and store native CAPTCHA keys in Serve, or inspect setup status; customer setup verifies Wix site ownership; does not enable member CAPTCHA SDKs", required: "setup|status --app <app> [--content <dir>] [--site <msid>] [--ios-bundle-id <id>] [--android-package-name <id>] [--target <t>]" },
143334
143334
  { name: "init", summary: "scaffold a starter content package (OTA-only; no native shells). --template <t> starts from a full reference app (branded-lite = consumer storefront over a Wix site; owner-lite = owner CRUD/management) instead of the minimal counter; --templates lists bundled ones; --remote fetches the template off the serve plane (falls back to the bundled copy offline). The new app also gets its agent files (the same set `himi agent-init` writes: AGENTS.md, CLAUDE.md, the himalaya skill, HIMALAYA.md, a SessionStart hook) so the next session started inside it picks the work up; --no-agent skips them. It also makes the build board (.himi/studio/, exactly as `himi studio --no-open` does \u2014 it never opens a browser), so every later render, test, validate, push and progress entry refreshes it; and, for a template bundled with this CLI, it makes the FIRST PICTURE itself (never for a --remote one: that is its publisher's code, and a render runs it on this machine, so next[] leaves the render to the agent after it has read the code): exactly `himi render <homeScreenId> --png --mocks --device iphone` (offline \u2014 no request; never --live; --mocks loads the app's recorded data when it has any), run as a child it kills after a time budget, writing .himi/shots/<home>.png and journaling it (by: himi) as the STARTING POINT (\"Starting point: the <template> template, before any of your changes\") so the board and the journal hold a picture when init returns. init also records the source it wrote (.himi/init-source.json: a content fingerprint, Markdown left out), and the first home PNG render after the source diverges from it is journaled once as \"First look at your app\" \u2014 reported as `firstLook: {image}` or `{skipped: <reason>}`, never a failed init; `--no-first-look` skips it (for scripts and CI that render themselves). For a bundled template (never a --remote one \u2014 the preview would build and serve its publisher's code) init also STARTS the live preview itself: `himi run --watch --content <dir> --port <p> --no-open --idle-exit 30`, detached in its own process group with its output in .himi/watch.log, never waited for (the url is chosen before it builds), recorded in .himi/watch.json, and reported as `watch: {started, pid, url, board, log}` \u2014 or `{skipped: <reason>}` for a `CI` environment, a test runner, a watch already running, or a start that failed (never a failed init). Nothing else skips it: outside CI it is started, and `watch.note` says an opt-out was ignored. The envelope LEADS with `doNow`: three lines to run now (the consent preflight, the live preview \u2014 its url and board when init started it, else `himi run --watch --content <dir> &` with the board's path \u2014 and the postcard line), and ENDS with the same lines as `doNowAgain`, so an agent that reads only the head or only the tail (`| tail`) gets them; at a terminal they are also printed as plain text on stderr, before the JSON and again after it. Its `next[]` LEADS with the steps for THIS session, in this order for every app \u2014 the offline first render and that picture's `himi progress` postcard (only when init could not make the picture), the skill's consent preflight (authorize or mock any Wix or external startup request; with none, go straight on), and only then the live preview (the running one's url, or `himi run --watch &` when init did not start it) \u2014 ahead of the next-session guidance", required: "<name> [--dir] [--template <t>] [--remote] [--base44-app-id <id>] [--base44-app-url <https://tenant>] [--no-agent] [--no-first-look] | --templates" },
@@ -157546,11 +157546,11 @@ function deriveAccentText(accent, surfaces) {
157546
157546
  const all = (typeof surfaces === "string" ? [surfaces] : [...surfaces]).map((s2) => s2.slice(0, 7));
157547
157547
  const page = all[0];
157548
157548
  const worst2 = (color) => Math.min(...all.map((s2) => contrastRatio4(color, s2)));
157549
- const opaque = accent.length === 9 ? mix2(accent.slice(0, 7), page, channel2(accent, 7) / 255) : accent;
157550
- if (worst2(opaque) >= ACCENT_TEXT_MIN_CONTRAST) return opaque;
157549
+ const opaque2 = accent.length === 9 ? mix2(accent.slice(0, 7), page, channel2(accent, 7) / 255) : accent;
157550
+ if (worst2(opaque2) >= ACCENT_TEXT_MIN_CONTRAST) return opaque2;
157551
157551
  const pole = worst2("#FFFFFF") >= worst2("#000000") ? "#FFFFFF" : "#000000";
157552
157552
  for (let step = 1; step <= 100; step++) {
157553
- const candidate = mix2(pole, opaque, step / 100);
157553
+ const candidate = mix2(pole, opaque2, step / 100);
157554
157554
  if (worst2(candidate) >= ACCENT_TEXT_MIN_CONTRAST) return candidate;
157555
157555
  }
157556
157556
  if (all.length > 1) {
@@ -171783,6 +171783,14 @@ var init_graph = __esm({
171783
171783
  });
171784
171784
 
171785
171785
  // src/one-app/design-rules.ts
171786
+ function typographyRole(name) {
171787
+ return /(title|heading|headline|display|hero)/i.test(name) ? "heading" : "body";
171788
+ }
171789
+ function sourceFontFamily(id, role) {
171790
+ if (id === "madefor") return role === "heading" ? "Wix Madefor Display" : "Wix Madefor Text";
171791
+ if (id === "system") return SYSTEM_FONT_FAMILY;
171792
+ return Object.hasOwn(SOURCE_FONT_FAMILIES, id) ? SOURCE_FONT_FAMILIES[id] : void 0;
171793
+ }
171786
171794
  function sourceMode(manifest2, raw) {
171787
171795
  const resolved2 = obj3(manifest2.source)?.resolvedAppInstance;
171788
171796
  if (resolved2 === "branded") return "branded";
@@ -171829,6 +171837,15 @@ function sourceDesign(raw, mode) {
171829
171837
  if (mode === "branded") {
171830
171838
  add("colors.surfacePrimary.light", "/settings/theme/colors/background", "color", colors.background);
171831
171839
  add("colors.textPrimary.light", "/settings/theme/colors/content", "color", colors.content);
171840
+ const primary = fields.find((field) => field.field === "colors.accentPrimary.light");
171841
+ if (primary) {
171842
+ add("colors.textOnAccent.light", primary.pointer, "contrast", String(primary.value));
171843
+ add("colors.accentText.light", primary.pointer, "contrast", String(primary.value));
171844
+ }
171845
+ const typography = obj3(theme.typography) ?? {};
171846
+ for (const role of ["heading", "body"]) {
171847
+ add(`typography.${role}.family`, `/settings/theme/typography/${role}`, "font", typography[role]);
171848
+ }
171832
171849
  const button = obj3(theme.button) ?? {};
171833
171850
  for (const variant of BUTTON_VARIANTS2) {
171834
171851
  const style = obj3(button[variant]);
@@ -171848,9 +171865,33 @@ function sourceDesign(raw, mode) {
171848
171865
  leafPointers(theme, "/settings/theme", all);
171849
171866
  return { mode, fields, notCompared: all.filter((pointer) => !read.has(pointer)).sort() };
171850
171867
  }
171851
- var BUTTON_VARIANTS2, obj3, seg;
171868
+ var SOURCE_FONT_FAMILIES, BUTTON_VARIANTS2, obj3, seg;
171852
171869
  var init_design_rules = __esm({
171853
171870
  "src/one-app/design-rules.ts"() {
171871
+ init_tokens();
171872
+ SOURCE_FONT_FAMILIES = {
171873
+ lato: "Lato",
171874
+ poppins: "Poppins",
171875
+ notoSans: "Noto Sans",
171876
+ inconsolata: "Inconsolata",
171877
+ vollkorn: "Vollkorn",
171878
+ playfairDisplay: "Playfair Display",
171879
+ bebasNeue: "Bebas Neue",
171880
+ lobster: "Lobster",
171881
+ brygada1918: "Brygada 1918",
171882
+ spaceGrotesk: "Space Grotesk",
171883
+ rubik: "Rubik",
171884
+ caudex: "Caudex",
171885
+ fahkwang: "Fahkwang",
171886
+ forum: "Forum",
171887
+ leagueSpartan: "League Spartan",
171888
+ marcellus: "Marcellus",
171889
+ marmelad: "Marmelad",
171890
+ pacifico: "Pacifico",
171891
+ questrial: "Questrial",
171892
+ sora: "Sora",
171893
+ workSans: "Work Sans"
171894
+ };
171854
171895
  BUTTON_VARIANTS2 = ["primary", "secondary"];
171855
171896
  obj3 = (value2) => value2 !== null && typeof value2 === "object" && !Array.isArray(value2) ? value2 : void 0;
171856
171897
  seg = (key3) => key3.replaceAll("~", "~0").replaceAll("/", "~1");
@@ -172562,12 +172603,59 @@ function readButtonToken(tokens2, variant, property2) {
172562
172603
  const values = new Set(drawn.map((entry) => entry.radius));
172563
172604
  return values.size === 1 ? { value: drawn[0].radius, origin } : { value: drawn.map((entry) => `${entry.size} ${entry.radius}`).join(", "), origin };
172564
172605
  }
172606
+ function opaque(value2) {
172607
+ const hex2 = value2 ? canonicalHex(value2) : void 0;
172608
+ return hex2?.length === 7 ? hex2 : hex2?.endsWith("FF") ? hex2.slice(0, 7) : void 0;
172609
+ }
172610
+ function contrastRow(field, tokens2, base) {
172611
+ const accent = opaque(readColorToken(tokens2, "accentPrimary").value);
172612
+ const isFill = field.field === "colors.textOnAccent.light";
172613
+ const background = isFill ? accent : opaque(readColorToken(tokens2, "surfacePrimary").value);
172614
+ if (!accent || !background) return { ...base, status: "unresolved", note: !accent ? "Accent contrast requires an opaque target accentPrimary token." : "Author an opaque surfacePrimary token before checking accent text; native hosts do not share the web's white page fallback." };
172615
+ const name = isFill ? "textOnAccent" : "accentText";
172616
+ const read = readColorToken(tokens2, name);
172617
+ const target = opaque(read.value ?? (isFill ? "#FFFFFF" : accent));
172618
+ const neutral = isFill ? [] : (textSurfacesOf(obj5(tokens2.colors) ?? {}).light ?? []).map((value2) => opaque(value2));
172619
+ if (neutral.some((value2) => !value2)) return { ...base, status: "unresolved", note: "Accent text contrast requires opaque neutral surface tokens." };
172620
+ const surfaces = [background, ...neutral];
172621
+ const desired = isFill ? contrastRatio3("#FFFFFF", accent) >= 4.5 ? "#FFFFFF" : "#000000" : deriveAccentText(accent, surfaces);
172622
+ const ratio = target ? Math.min(...surfaces.map((surface) => contrastRatio3(target, surface))) : 0;
172623
+ if (ratio < 4.5 && surfaces.some((surface) => contrastRatio3(desired, surface) < 4.5)) {
172624
+ return { ...base, status: "unresolved", note: "No single derived accent text shade reaches 4.5:1 on every neutral surface; author separate local text roles for the conflicting backdrops." };
172625
+ }
172626
+ return {
172627
+ ...base,
172628
+ source: desired,
172629
+ ...target ? { target } : {},
172630
+ targetOrigin: read.origin === "absent" ? "renderer-default" : read.origin,
172631
+ status: target && ratio >= 4.5 ? "match" : "mismatch",
172632
+ note: `Accent text must reach 4.5:1; observed ${ratio.toFixed(2)}:1.`
172633
+ };
172634
+ }
172565
172635
  function compare3(field, tokens2) {
172566
172636
  const id = requirementId("design", field.pointer, field.field);
172567
172637
  const base = { id, field: field.field, sourcePath: field.pointer, source: field.value };
172568
172638
  if (field.kind === "unsupported") {
172569
172639
  return { ...base, status: "unresolved", note: field.note };
172570
172640
  }
172641
+ if (field.kind === "contrast") return contrastRow(field, tokens2, { ...base, status: "mismatch" });
172642
+ if (field.kind === "font") {
172643
+ const role = field.field.split(".")[1];
172644
+ const mapped = sourceFontFamily(String(field.value), role);
172645
+ const family = mapped === SYSTEM_FONT_FAMILY ? mapped : mapped ? canonicalFamilyName(mapped) : void 0;
172646
+ const entries = Object.entries(obj5(tokens2.typography) ?? {}).filter(([name]) => typographyRole(name) === role);
172647
+ if (!family || !entries.length || entries.some(([, entry]) => !obj5(entry))) {
172648
+ return { ...base, status: "unresolved", note: !family ? mapped ? `Source font ${mapped} is not in the Himi font catalog; resolve a supported family before importing it.` : "Unknown source font ID; resolve its family before importing it." : "No inspectable target typography entries for this role; author numeric metrics before importing the family." };
172649
+ }
172650
+ const wrong = entries.filter(([, entry]) => obj5(entry)?.family !== family);
172651
+ return {
172652
+ ...base,
172653
+ source: family,
172654
+ target: wrong.length ? wrong.map(([name, entry]) => `${name}: ${obj5(entry)?.family ?? "system"}`).join(", ") : family,
172655
+ targetOrigin: "token",
172656
+ status: wrong.length ? "mismatch" : "match"
172657
+ };
172658
+ }
172571
172659
  const parts = field.field.split(".");
172572
172660
  const read = field.kind === "color" ? readColorToken(tokens2, parts[1]) : readButtonToken(tokens2, parts[3], parts[4]);
172573
172661
  const row2 = { ...base, status: "mismatch", targetOrigin: read.origin, ...read.value !== void 0 ? { target: read.value } : {} };
@@ -172618,6 +172706,10 @@ function applyFix(tokens2, rows) {
172618
172706
  const previous = typeof existing === "string" ? existing : obj5(existing)?.light;
172619
172707
  const entry = obj5(existing) ?? {};
172620
172708
  colors[parts[1]] = typeof previous === "string" ? { ...entry, light: row2.source, dark: entry.dark ?? previous, highContrast: entry.highContrast ?? entry.dark ?? previous } : { ...entry, light: row2.source };
172709
+ } else if (parts[0] === "typography") {
172710
+ for (const [name, entry] of Object.entries(obj5(tokens2.typography) ?? {})) {
172711
+ if (typographyRole(name) === parts[1] && obj5(entry)) entry.family = row2.source;
172712
+ }
172621
172713
  } else {
172622
172714
  const variant = ensure2(ensure2(ensure2(ensure2(tokens2, "controls"), "button"), "variants"), parts[3]);
172623
172715
  variant[parts[4]] = row2.source;
@@ -172809,7 +172901,7 @@ function validateDesign(evidenceDir, contentDir2, options = {}) {
172809
172901
  const adapted = waivers(contentDir2, hashes);
172810
172902
  const evaluate2 = () => design.fields.map((field) => {
172811
172903
  const row2 = compare3(field, tokens2);
172812
- const waiver = adapted.get(row2.id);
172904
+ const waiver = field.kind === "contrast" || field.kind === "font" && row2.status === "unresolved" ? void 0 : adapted.get(row2.id);
172813
172905
  if (row2.status === "unresolved") return waiver ? { ...row2, status: "adapted" } : row2;
172814
172906
  let overlayDifference;
172815
172907
  let waivedOverlay = false;
@@ -172819,7 +172911,7 @@ function validateDesign(evidenceDir, contentDir2, options = {}) {
172819
172911
  if (!overlay) continue;
172820
172912
  const drawn = compare3(field, mergeIdiomOverlay(tokens2, overlay));
172821
172913
  if (drawn.status === "match") continue;
172822
- if (waiver?.has(name)) {
172914
+ if (waiver?.has(name) && !(field.kind === "font" && drawn.status === "unresolved")) {
172823
172915
  waivedOverlay = true;
172824
172916
  continue;
172825
172917
  }
@@ -172839,7 +172931,34 @@ function validateDesign(evidenceDir, contentDir2, options = {}) {
172839
172931
  let rows = evaluate2();
172840
172932
  let fixed = [];
172841
172933
  if (options.fix) {
172842
- fixed = applyFix(tokens2, rows);
172934
+ if (design.fields.some((field) => field.kind === "contrast") && !opaque(readColorToken(tokens2, "accentPrimary").value)) {
172935
+ return {
172936
+ ok: false,
172937
+ code: "design_unresolved",
172938
+ ...base,
172939
+ rows,
172940
+ fixed: [],
172941
+ unresolved: ["Author an opaque target accentPrimary palette before --fix; its existing dark/high-contrast appearance cannot be preserved from a missing or translucent accent."]
172942
+ };
172943
+ }
172944
+ const previousColors = structuredClone(obj5(tokens2.colors) ?? {});
172945
+ const contrastFields = new Set(design.fields.filter((field) => field.kind === "contrast").map((field) => field.field));
172946
+ fixed = applyFix(tokens2, rows.filter((row2) => !contrastFields.has(row2.field)));
172947
+ const contrastRows = evaluate2().filter((row2) => contrastFields.has(row2.field));
172948
+ for (const row2 of contrastRows) {
172949
+ const name = row2.field.split(".")[1];
172950
+ if (row2.status !== "mismatch" || row2.targetOrigin === "idiom-token" || previousColors[name] !== void 0) continue;
172951
+ const fallback = name === "textOnAccent" ? "#FFFFFF" : previousColors.accentPrimary;
172952
+ const prior = typeof fallback === "string" ? { light: fallback } : obj5(fallback);
172953
+ if (prior && typeof prior.light === "string") {
172954
+ tokens2.colors[name] = {
172955
+ light: prior.light,
172956
+ dark: prior.dark ?? prior.light,
172957
+ highContrast: prior.highContrast ?? prior.dark ?? prior.light
172958
+ };
172959
+ }
172960
+ }
172961
+ fixed.push(...applyFix(tokens2, contrastRows));
172843
172962
  if (fixed.length) {
172844
172963
  try {
172845
172964
  writeFixedTokens(contentDir2, location, tokensPath, tokens2);
@@ -172855,9 +172974,10 @@ function validateDesign(evidenceDir, contentDir2, options = {}) {
172855
172974
  }
172856
172975
  }
172857
172976
  const mismatches = rows.filter((row2) => row2.status === "mismatch");
172977
+ const unresolvedRequired = rows.some((row2) => row2.status === "unresolved" && design.fields.some((field) => (field.kind === "contrast" || field.kind === "font") && field.field === row2.field));
172858
172978
  return {
172859
- ok: mismatches.length === 0,
172860
- ...mismatches.length ? { code: "design_mismatch" } : {},
172979
+ ok: mismatches.length === 0 && !unresolvedRequired,
172980
+ ...mismatches.length ? { code: "design_mismatch" } : unresolvedRequired ? { code: "design_unresolved" } : {},
172861
172981
  ...base,
172862
172982
  rows,
172863
172983
  mismatches: mismatches.map((row2) => `${row2.field}: source ${row2.source} vs target ${row2.target ?? "absent"}`),
@@ -172986,10 +173106,12 @@ var init_validate_design = __esm({
172986
173106
  init_token_colors();
172987
173107
  init_token_lint();
172988
173108
  init_tokens();
173109
+ init_catalog2();
172989
173110
  init_idiom();
172990
173111
  init_preview_surfaces();
172991
173112
  init_config_source_edit();
172992
173113
  init_coverage2();
173114
+ init_brand_color_math();
172993
173115
  init_design_rules();
172994
173116
  init_progress();
172995
173117
  USAGE3 = "himi one-app validate-design --evidence-dir <.himi/one-app> --content <app-dir> [--json] [--fix] [--record --release-hash <sha256>]";
@@ -193078,10 +193200,10 @@ function tokensFromWst(wst) {
193078
193200
  const cream = accent2 && isNearWhite(accent2) ? accent2 : void 0;
193079
193201
  const surface2 = base2 && !isNearBlack(base2) ? base2 : cream;
193080
193202
  if (surface2) colors.surfaceSecondary = { light: surface2 };
193081
- const opaque = (c2) => c2 && c2.length === 7 ? c2 : void 0;
193082
- const surface = opaque(usableCssColor(wst["--wst-color-fill-base-1"]));
193083
- const ramp = !surface ? [] : ["--wst-color-fill-base-shade-1", "--wst-color-fill-base-shade-2", "--wst-color-fill-base-shade-3"].map((v2) => opaque(usableCssColor(wst[v2]))).filter((c2) => !!c2).map((c2) => ({ c: c2, ratio: contrastRatio3(c2, surface) }));
193084
- const primary = surface ? opaque(usableCssColor(wst["--wst-color-text-primary"])) : void 0;
193203
+ const opaque2 = (c2) => c2 && c2.length === 7 ? c2 : void 0;
193204
+ const surface = opaque2(usableCssColor(wst["--wst-color-fill-base-1"]));
193205
+ const ramp = !surface ? [] : ["--wst-color-fill-base-shade-1", "--wst-color-fill-base-shade-2", "--wst-color-fill-base-shade-3"].map((v2) => opaque2(usableCssColor(wst[v2]))).filter((c2) => !!c2).map((c2) => ({ c: c2, ratio: contrastRatio3(c2, surface) }));
193206
+ const primary = surface ? opaque2(usableCssColor(wst["--wst-color-text-primary"])) : void 0;
193085
193207
  const primaryRatio = primary && surface ? contrastRatio3(primary, surface) : void 0;
193086
193208
  const tertiary = primaryRatio === void 0 ? void 0 : ramp.filter((x2) => x2.ratio >= 4.5 && x2.ratio < primaryRatio).sort((a2, b2) => a2.ratio - b2.ratio)[0];
193087
193209
  if (tertiary) colors.textTertiary = { light: tertiary.c };
@@ -198899,7 +199021,7 @@ async function runOneAppCommand(argv, io2, deps = {}, commandIncluded = false) {
198899
199021
 
198900
199022
  Collect the source mobile app and hand it to the current coding agent. The agent continues with a faithful plan, local generation and a checked hosted draft. Branded falls back to Free (Spaces) only when absent. This command itself does not launch AI or build an app. --dir is the parent directory; an existing destination is refused. --top-tabs bottom-tabs (default) plans bottom tabs, one per visible page, for a source with top tabs; keep retains the top tabs.
198901
199023
 
198902
- validate-topbar checks authored source top-bar controls against the migrated config.ts. validate-design compares the source theme's colors and button shape with the migrated tokens.json, repairs the mismatches with --fix, and records a passing check for the coverage ledger with --record. Both are scoped to a One App evidence directory and do not change general himi validate behavior.`);
199024
+ validate-topbar checks authored source top-bar controls against the migrated config.ts. validate-design compares the source theme's colors, heading/body font families, button shape and accent text contrast with the migrated tokens.json, repairs the mismatches with --fix, and records a passing check for the coverage ledger with --record. Both are scoped to a One App evidence directory and do not change general himi validate behavior.`);
198903
199025
  return 0;
198904
199026
  }
198905
199027
  const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
@@ -163,20 +163,45 @@ Apply these interpretation rules:
163
163
  a feature to account for; they do not alone prove visibility or access for every
164
164
  visitor or member. Resolve those rules separately. This check is specific to this
165
165
  migration workflow; it does not change the behavior of `himi validate`.
166
- - Before publishing, run the migration-only design check:
166
+ - Apply the collected app theme before the first build: run
167
+ `himi one-app validate-design --evidence-dir .himi/one-app --content . --fix`,
168
+ resolve any remaining mismatches/unresolved values, then rebuild. The source is
169
+ `settings.theme`, not the website or the starter template. Before publishing,
170
+ run the migration-only design check:
167
171
  `himi one-app validate-design --evidence-dir .himi/one-app --content .`.
168
172
  It resolves the effective source theme with the reader the source app instance
169
- actually uses, then compares the resolved primary, background and content colors
170
- and each button's `borderRadius` and `outlineWidth` against the app's `tokens.json`.
173
+ actually uses, then compares the resolved primary, background and content colors,
174
+ each button's `borderRadius` and `outlineWidth`, and resolved heading/body font families
175
+ against the app's `tokens.json`.
171
176
  Hex case and shorthand are normalized; an absent button token is compared with the
172
177
  value the renderer draws (an outline only on `secondary`; a radius of `radius.pill`,
173
178
  else 24), not called a difference. A variant without its own radius must draw the
174
179
  source radius at every Button size. A descriptor's per-button `cornerRadius` overrides
175
- the tokens and is not checked. `--fix` writes the mismatching source values into
180
+ the tokens and is not checked; descriptor/control-specific fill and label overrides
181
+ also need a rendered check. `--fix` writes the mismatching source values into
176
182
  `tokens.json` and pins the dark and high-contrast schemes to what they drew before,
177
183
  because a missing scheme falls back to the light value. Rebuild after `--fix`.
184
+ For Branded apps, known source typography IDs (for example `lato`) become family
185
+ names (`Lato`); tokens whose names contain title/heading/headline/display/hero use the heading family; other
186
+ typography tokens use the body family. Numeric metrics stay unchanged. Unknown IDs,
187
+ families outside the Himi font catalog, and missing target typography roles remain unresolved; resolve them before recording
188
+ a pass, even when the coverage ledger cites an adaptation. A deliberate difference
189
+ between resolved font roles can still be adapted. Build/font validation still has
190
+ to prove the font bytes are available. Unresolved fonts and contrast also fail
191
+ the plain check used by push, not only `--record`.
192
+ Branded accent text must reach 4.5:1: `textOnAccent` against the fill, and `accentText` against the target page and declared neutral
193
+ surfaces. `--fix` chooses readable ink and derives a same-hue text shade while
194
+ retaining the exact source accent fill. For example, `#D6BD07` needs dark labels;
195
+ `#7A6C00` is a readable accent text shade on white. Translucent target colors cannot earn
196
+ an opaque contrast claim; an explicitly adapted source color still needs independently
197
+ readable opaque target roles. Author `surfacePrimary` explicitly when absent: native
198
+ hosts do not share the web's white page fallback. Conflicting page/card backdrops with no derived shade
199
+ that clears every surface stay unresolved; use separate local text roles. A missing
200
+ or translucent target accent palette must be authored explicitly before `--fix`,
201
+ because its non-light appearance cannot be preserved safely. Source `system` font
202
+ IDs use the portable `$system` sentinel. Contrast failures cannot be waived by a ledger adaptation.
178
203
  The report names every theme pointer the check did not compare, including typography
179
- kits and any button `uppercase`, which Himi tokens cannot carry; resolve those
204
+ kit IDs and any button `uppercase`, which Himi tokens cannot carry; resolve those
180
205
  explicitly rather than treating a pass as full design parity. A deliberate difference
181
206
  is waived only by a `design` coverage row with status `adapted` that cites the user's
182
207
  instruction and explains the change. This check compares values, not rendered screens.
@@ -23983,7 +23983,7 @@
23983
23983
  "name": "WixParticipantState",
23984
23984
  "kind": "type",
23985
23985
  "jsdoc": "/** Participant lifecycle state. */",
23986
- "decl": "export type WixParticipantState =\n | \"JOINED\"\n | \"PAYMENT_PENDING\"\n | \"APPROVAL_PENDING\"\n | \"COMPLETED\"\n | \"INVITED\"\n | \"REMOVED\"\n | string;"
23986
+ "decl": "export type WixParticipantState =\n | \"JOINED\"\n | \"PAYMENT_PENDING\"\n | \"ENROLLMENT_PENDING\"\n | \"APPROVAL_PENDING\"\n | \"COMPLETED\"\n | \"INVITED\"\n | \"REMOVED\"\n | string;"
23987
23987
  },
23988
23988
  {
23989
23989
  "name": "WixParticipantPaymentPending",
@@ -23994,7 +23994,7 @@
23994
23994
  {
23995
23995
  "name": "WixJoinedParticipant",
23996
23996
  "kind": "interface",
23997
- "jsdoc": "/**\n * The participant JoinProgram hands back — a DIFFERENT entity from the roster\n * `WixParticipant` above, which is why it gets its own name. This one describes the join\n * outcome (`state`, `paymentPendingOptions`); the roster one describes who the member is\n * and how far they have got (`member`, `progress`, `enrollmentStatus`).\n *\n * Both were spelled `WixParticipant` until #2289, so TypeScript MERGED the two declarations\n * instead of keeping them apart. That is wrong twice over: the merged type carries every\n * field of both, so it describes neither response — and the merge is itself an error,\n * because `id` was required in the roster shape and optional here (TS2687 + TS2717).\n */",
23997
+ "jsdoc": "/**\n * The compatibility result joinProgram returns — a DIFFERENT entity from the roster\n * `WixParticipant` above, which is why it gets its own name. This one describes the join\n * outcome (`state`, `paymentPendingOptions`); the roster one describes who the member is\n * and how far they have got (`member`, `progress`, `enrollmentStatus`).\n *\n * Both were spelled `WixParticipant` until #2289, so TypeScript MERGED the two declarations\n * instead of keeping them apart. That is wrong twice over: the merged type carries every\n * field of both, so it describes neither response — and the merge is itself an error,\n * because `id` was required in the roster shape and optional here (TS2687 + TS2717).\n */",
23998
23998
  "decl": "export interface WixJoinedParticipant {\n id?: string;\n programId?: string;\n memberId?: string;\n state?: WixParticipantState;\n paymentPendingOptions?: WixParticipantPaymentPending;\n joinedOptions?: { joinedDate?: string };\n}"
23999
23999
  },
24000
24000
  {
@@ -24006,14 +24006,14 @@
24006
24006
  {
24007
24007
  "name": "joinProgram",
24008
24008
  "kind": "function",
24009
- "jsdoc": "/** Join a program as the signed-in member. Branch on `participant.state`. */",
24010
- "decl": "export function joinProgram(programId: string): Promise<WixJoinProgramResponse>;",
24011
- "http": "POST /online-programs/v3/participants/{…}/join"
24009
+ "jsdoc": "/**\n * Join as the signed-in member through Join Applications. Read the current\n * application first so an existing application is resumed without another create.\n * Approved paid applications prepare\n * SINGLE_PAYMENT and return the Wix Payments order as paymentPendingOptions.orderId.\n * mayPreparePayment rechecks the caller's checkout guard before creating that order.\n * @scope member\n */",
24010
+ "decl": "export async function joinProgram(programId: string, options?: { mayPreparePayment?: () => boolean }): Promise<WixJoinProgramResponse>;",
24011
+ "scope": "member"
24012
24012
  },
24013
24013
  {
24014
24014
  "name": "payableIdOf",
24015
24015
  "kind": "function",
24016
- "jsdoc": "/** The cashier handle to settle, or \"\" when the join needs no payment. */",
24016
+ "jsdoc": "/** The cashier handle to settle, or \"\" when no payable order is available. */",
24017
24017
  "decl": "export function payableIdOf(participant?: WixJoinedParticipant): string;"
24018
24018
  }
24019
24019
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1558.0",
3
+ "version": "0.1560.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": "3e25c3d6c9c9f3a1d3abe87df97e8a1f848ce1a712d321126ce42454"
74
+ "falconPackageHash": "4a9c5b9b83751ee0bf714c03ed0abc9d8738a205eed4a022bdec8fcb"
75
75
  }
@@ -9,7 +9,7 @@ import { withOnlineWrites } from "../_shared/online-actions.js";
9
9
  // error card.
10
10
  //
11
11
  // Phase 2d — enrolling is NATIVE. `joinProgram` covers all three program kinds in
12
- // one call and the outcome is read off `participant.state`, not assumed:
12
+ // the Join Applications API; its compatibility result exposes only confirmed outcomes:
13
13
  // a free/open program comes back JOINED with nothing to pay, a paid one comes back
14
14
  // PAYMENT_PENDING and settles on the shared in-app pay rail, and a private one comes
15
15
  // back APPROVAL_PENDING (the owner approves; there is nothing to charge). Any failure
@@ -190,18 +190,26 @@ interface CommerceGuards {
190
190
  async function enrollmentMessage(name: string, guards: CommerceGuards): Promise<string> {
191
191
  const blocked = () => iapPriceCopy("program").purchaseUnavailable;
192
192
  if (!guards.mayStart()) return blocked();
193
+ let preparationWithdrawn = false;
193
194
  try {
194
- const joined = await joinProgram(inputId());
195
+ const joined = await joinProgram(inputId(), { mayPreparePayment: () => {
196
+ if (!guards.mayContinue()) preparationWithdrawn = true;
197
+ return !preparationWithdrawn;
198
+ } });
195
199
  const participant = joined.participant;
196
200
  const programState = (participant?.state ?? "").toUpperCase();
197
201
  if (programState === "APPROVAL_PENDING") {
198
202
  return copyParam("program.approvalPendingMessage", "Request sent — the host will approve your place.");
199
203
  }
204
+ if (programState === "ENROLLMENT_PENDING") {
205
+ return copyParam("program.enrollmentPendingMessage", "Your enrollment is processing — check back shortly.");
206
+ }
200
207
  const payable = payableIdOf(participant);
201
- if (!payable) {
202
- // Free / open program (or one already settled) ⇒ nothing to charge.
208
+ if (programState === "JOINED") {
209
+ // Only a confirmed participant is enrolled; absent payment data is not success.
203
210
  return copyTemplate("program.joinedMessage", "You're enrolled in {name}.", { name });
204
211
  }
212
+ if (programState !== "PAYMENT_PENDING" || !payable) throw new Error("Program enrollment is not ready");
205
213
  const msid = await wixPlatformSession.signedInstanceStatus().then((s) => s.msid ?? "").catch(() => "");
206
214
  // Re-read before each step that commits money: a route that settles meanwhile wins.
207
215
  let withdrawn = false;
@@ -221,12 +229,8 @@ async function enrollmentMessage(name: string, guards: CommerceGuards): Promise<
221
229
  // which is precisely the dead-end the two-rail design exists to prevent
222
230
  // (and which the cart/plans/events flows each guard with a test).
223
231
  //
224
- // There is no `programsCheckout` redirect intent. `ecomOrderPaymentRequest`
225
- // is the only payment-request-shaped one, and a program's `purchaseFlowId`
226
- // is a payment-request handle, so that is what we hand it. If that mapping
227
- // is ever wrong the call throws, we degrade to the same site-directing
228
- // message as before, and nothing regresses — but when it is right, a
229
- // wallet-less site can actually sell the program IN-APP.
232
+ // Join Applications returns a Wix Payments order id; the payment-request
233
+ // redirect uses that same order handle on the hosted rail.
230
234
  cashierUrl: async () => {
231
235
  if (!stillAllowed()) return undefined;
232
236
  const redirect = await createRedirectSession({
@@ -237,10 +241,13 @@ async function enrollmentMessage(name: string, guards: CommerceGuards): Promise<
237
241
  },
238
242
  });
239
243
  if (withdrawn) return blocked();
240
- if (pay.kind === "paid") return copyTemplate("program.joinedMessage", "You're enrolled in {name}.", { name });
244
+ if (pay.kind === "paid") return pay.pending
245
+ ? copyParam("program.paymentPendingMessage", "Payment is processing — enrollment will be confirmed after payment.")
246
+ : copyTemplate("program.joinedMessage", "You're enrolled in {name}.", { name });
241
247
  if (pay.kind === "handoff") return "";
242
248
  if (pay.kind === "cancelled") return copyParam("program.joinCancelledMessage", "Payment cancelled — you haven't enrolled.");
243
249
  } catch {
250
+ if (preparationWithdrawn) return blocked();
244
251
  // fall through to the site-directing message
245
252
  }
246
253
  return copyTemplate("program.joinOnSiteMessage", "Enrolment happens on the website — visit the site to join {name}.", { name });
@@ -9,7 +9,7 @@ import { withOnlineWrites } from "../_shared/online-actions.js";
9
9
  // error card.
10
10
  //
11
11
  // Phase 2d — enrolling is NATIVE. `joinProgram` covers all three program kinds in
12
- // one call and the outcome is read off `participant.state`, not assumed:
12
+ // the Join Applications API; its compatibility result exposes only confirmed outcomes:
13
13
  // a free/open program comes back JOINED with nothing to pay, a paid one comes back
14
14
  // PAYMENT_PENDING and settles on the shared in-app pay rail, and a private one comes
15
15
  // back APPROVAL_PENDING (the owner approves; there is nothing to charge). Any failure
@@ -190,18 +190,26 @@ interface CommerceGuards {
190
190
  async function enrollmentMessage(name: string, guards: CommerceGuards): Promise<string> {
191
191
  const blocked = () => iapPriceCopy("program").purchaseUnavailable;
192
192
  if (!guards.mayStart()) return blocked();
193
+ let preparationWithdrawn = false;
193
194
  try {
194
- const joined = await joinProgram(inputId());
195
+ const joined = await joinProgram(inputId(), { mayPreparePayment: () => {
196
+ if (!guards.mayContinue()) preparationWithdrawn = true;
197
+ return !preparationWithdrawn;
198
+ } });
195
199
  const participant = joined.participant;
196
200
  const programState = (participant?.state ?? "").toUpperCase();
197
201
  if (programState === "APPROVAL_PENDING") {
198
202
  return copyParam("program.approvalPendingMessage", "Request sent — the host will approve your place.");
199
203
  }
204
+ if (programState === "ENROLLMENT_PENDING") {
205
+ return copyParam("program.enrollmentPendingMessage", "Your enrollment is processing — check back shortly.");
206
+ }
200
207
  const payable = payableIdOf(participant);
201
- if (!payable) {
202
- // Free / open program (or one already settled) ⇒ nothing to charge.
208
+ if (programState === "JOINED") {
209
+ // Only a confirmed participant is enrolled; absent payment data is not success.
203
210
  return copyTemplate("program.joinedMessage", "You're enrolled in {name}.", { name });
204
211
  }
212
+ if (programState !== "PAYMENT_PENDING" || !payable) throw new Error("Program enrollment is not ready");
205
213
  const msid = await wixPlatformSession.signedInstanceStatus().then((s) => s.msid ?? "").catch(() => "");
206
214
  // Re-read before each step that commits money: a route that settles meanwhile wins.
207
215
  let withdrawn = false;
@@ -221,12 +229,8 @@ async function enrollmentMessage(name: string, guards: CommerceGuards): Promise<
221
229
  // which is precisely the dead-end the two-rail design exists to prevent
222
230
  // (and which the cart/plans/events flows each guard with a test).
223
231
  //
224
- // There is no `programsCheckout` redirect intent. `ecomOrderPaymentRequest`
225
- // is the only payment-request-shaped one, and a program's `purchaseFlowId`
226
- // is a payment-request handle, so that is what we hand it. If that mapping
227
- // is ever wrong the call throws, we degrade to the same site-directing
228
- // message as before, and nothing regresses — but when it is right, a
229
- // wallet-less site can actually sell the program IN-APP.
232
+ // Join Applications returns a Wix Payments order id; the payment-request
233
+ // redirect uses that same order handle on the hosted rail.
230
234
  cashierUrl: async () => {
231
235
  if (!stillAllowed()) return undefined;
232
236
  const redirect = await createRedirectSession({
@@ -237,10 +241,13 @@ async function enrollmentMessage(name: string, guards: CommerceGuards): Promise<
237
241
  },
238
242
  });
239
243
  if (withdrawn) return blocked();
240
- if (pay.kind === "paid") return copyTemplate("program.joinedMessage", "You're enrolled in {name}.", { name });
244
+ if (pay.kind === "paid") return pay.pending
245
+ ? copyParam("program.paymentPendingMessage", "Payment is processing — enrollment will be confirmed after payment.")
246
+ : copyTemplate("program.joinedMessage", "You're enrolled in {name}.", { name });
241
247
  if (pay.kind === "handoff") return "";
242
248
  if (pay.kind === "cancelled") return copyParam("program.joinCancelledMessage", "Payment cancelled — you haven't enrolled.");
243
249
  } catch {
250
+ if (preparationWithdrawn) return blocked();
244
251
  // fall through to the site-directing message
245
252
  }
246
253
  return copyTemplate("program.joinOnSiteMessage", "Enrolment happens on the website — visit the site to join {name}.", { name });