@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.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/online-programs/src/index.d.ts +14 -5
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-online-programs.mjs +35 -7
- package/dist/cli.mjs +140 -18
- package/guides/one-app-to-himi-app.md +30 -5
- package/guides/sdk-reference.json +6 -6
- package/package.json +2 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +18 -11
- package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +18 -11
|
@@ -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
|
|
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
|
-
/**
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
23090
|
-
const words = (
|
|
23091
|
-
return `${words || screenId}${
|
|
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
|
|
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
|
|
157550
|
-
if (worst2(
|
|
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,
|
|
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
|
-
|
|
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
|
|
193082
|
-
const surface =
|
|
193083
|
-
const ramp = !surface ? [] : ["--wst-color-fill-base-shade-1", "--wst-color-fill-base-shade-2", "--wst-color-fill-base-shade-3"].map((v2) =>
|
|
193084
|
-
const primary = surface ?
|
|
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
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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": "
|
|
24010
|
-
"decl": "export function joinProgram(programId: string): Promise<WixJoinProgramResponse>;",
|
|
24011
|
-
"
|
|
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
|
|
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.
|
|
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": "
|
|
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
|
-
//
|
|
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 (
|
|
202
|
-
//
|
|
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
|
-
//
|
|
225
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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 (
|
|
202
|
-
//
|
|
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
|
-
//
|
|
225
|
-
//
|
|
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
|
|
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 });
|