@wix/himalaya-cli 0.1551.0 → 0.1553.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/core/dev-server/src/builder.d.ts +3 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments-contract.d.ts +2 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/apple-iap-contract.d.ts +24 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/order-refs.d.ts +12 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/forms/src/extensions.d.ts +83 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +146 -23
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +8 -5
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +147 -24
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-forms.mjs +19 -0
- package/dist/cli.mjs +2513 -2201
- package/dist/preview-assets.generated.bin +0 -0
- package/fixtures/wix-canned.json +14 -7
- package/guides/getting-started.md +17 -1
- package/guides/headless-loop.md +7 -1
- package/guides/himi-skill.md +17 -1
- package/guides/himi-test.md +95 -7
- package/guides/iap-pricing.md +1 -1
- package/guides/iap-products.md +2 -2
- package/guides/iap-screen-adoption.md +1 -1
- package/guides/index.json +1 -1
- package/guides/sdk-reference.json +82 -4
- package/guides/skill.md +16 -0
- package/package.json +2 -2
- package/templates/beauty-salon/config.ts +13 -0
- package/templates/beauty-salon/tier5-src/_shared/iap-attendance-purchase.ts +1 -1
- package/templates/beauty-salon/tier5-src/_shared/iap-worker.ts +1 -1
- package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +1 -1
- package/templates/branded-lite/config.ts +13 -0
- package/templates/branded-lite/dev/index.ts +1338 -2011
- package/templates/branded-lite/tier5-src/_shared/branded-form.ts +262 -0
- package/templates/branded-lite/tier5-src/_shared/iap-attendance-purchase.ts +1 -1
- package/templates/branded-lite/tier5-src/_shared/iap-worker.ts +1 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-form/index.ts +399 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-forms/index.ts +22 -7
- package/templates/branded-lite/tier5-src/himi-store-product/index.ts +1 -1
- package/templates/community-club/config.ts +13 -0
- package/templates/community-club/tier5-src/_shared/iap-attendance-purchase.ts +1 -1
- package/templates/community-club/tier5-src/_shared/iap-worker.ts +1 -1
- package/templates/events-meetups/config.ts +13 -0
- package/templates/events-meetups/tier5-src/_shared/iap-attendance-purchase.ts +1 -1
- package/templates/events-meetups/tier5-src/_shared/iap-worker.ts +1 -1
- package/templates/gym-fitness/config.ts +13 -0
- package/templates/gym-fitness/tier5-src/_shared/iap-attendance-purchase.ts +1 -1
- package/templates/gym-fitness/tier5-src/_shared/iap-worker.ts +1 -1
- package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +1 -1
- package/templates/nonprofit-cause/config.ts +13 -0
- package/templates/nonprofit-cause/tier5-src/_shared/iap-attendance-purchase.ts +1 -1
- package/templates/nonprofit-cause/tier5-src/_shared/iap-worker.ts +1 -1
- package/templates/podcast-creator/config.ts +13 -0
- package/templates/podcast-creator/tier5-src/_shared/iap-attendance-purchase.ts +1 -1
- package/templates/podcast-creator/tier5-src/_shared/iap-worker.ts +1 -1
- package/templates/portfolio-creative/config.ts +13 -0
- package/templates/portfolio-creative/tier5-src/_shared/iap-attendance-purchase.ts +1 -1
- package/templates/portfolio-creative/tier5-src/_shared/iap-worker.ts +1 -1
- package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +1 -1
- package/templates/restaurant/config.ts +13 -0
- package/templates/restaurant/tier5-src/_shared/iap-attendance-purchase.ts +1 -1
- package/templates/restaurant/tier5-src/_shared/iap-worker.ts +1 -1
- package/templates/retail-store/config.ts +13 -0
- package/templates/retail-store/tier5-src/_shared/iap-worker.ts +1 -1
- package/templates/retail-store/tier5-src/himi-store-product/index.ts +1 -1
- package/templates/yoga-studio/config.ts +13 -0
- package/templates/yoga-studio/tier5-src/_shared/iap-attendance-purchase.ts +1 -1
- package/templates/yoga-studio/tier5-src/_shared/iap-worker.ts +1 -1
- package/templates/branded-lite/tier5-src/wix-spaces-form-detail/index.ts +0 -30
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// himi-branded-lite-form — the Branded Lite Form screen: one Wix Form a visitor fills
|
|
2
|
+
// out and sends.
|
|
3
|
+
//
|
|
4
|
+
// Reads the form (getForm) and renders its fields in layout order, one page at a time
|
|
5
|
+
// (_shared/branded-form.ts parses the schema). A signed-in member gets email, names and
|
|
6
|
+
// phone filled in from their member record, matched by each field's contact mapping.
|
|
7
|
+
// Send = the app's own checks → validateFormSubmission (Wix's checks, nothing created) →
|
|
8
|
+
// createSubmission, opted into the offline outbox. Server errors land in each field's own
|
|
9
|
+
// slot; a failed send keeps every value and says so by the button.
|
|
10
|
+
//
|
|
11
|
+
// A form with a field the app cannot fill, or a conditional rule, shows a notice that
|
|
12
|
+
// opens the website instead (a hidden required field would fail the whole send). A form
|
|
13
|
+
// whose deadline passed shows a closed notice. Replaces wix-spaces-form-detail for this
|
|
14
|
+
// app only: the Spaces clone fixture still uses that worker.
|
|
15
|
+
import { classifyLoadError, defineService } from "@himalaya/state";
|
|
16
|
+
import { open as openUrl } from "@himalaya/open-url";
|
|
17
|
+
import { createSubmission, getForm, validateFormSubmission } from "@himalaya/wix-forms";
|
|
18
|
+
import { getMyMember } from "@himalaya/wix-members";
|
|
19
|
+
import { copyParam, copyTemplate, stashCopy } from "../_shared/himi-copy.js";
|
|
20
|
+
import { detailInputId, presence, presenceAfterError, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
21
|
+
import { memberAccess } from "../_shared/member-access.js";
|
|
22
|
+
import { formatDateMedium } from "../_shared/engine-intl.js";
|
|
23
|
+
import {
|
|
24
|
+
EMPTY_VALUE, isoDate, localProblems, parseForm, serverProblems, submissionValues,
|
|
25
|
+
type FieldValue, type FormFieldModel, type FormModel,
|
|
26
|
+
} from "../_shared/branded-form.js";
|
|
27
|
+
|
|
28
|
+
interface OptionRow { id: string; target: string; value: string; label: string; selected: boolean }
|
|
29
|
+
interface FieldRow {
|
|
30
|
+
id: string; target: string;
|
|
31
|
+
label: string; hideLabel: boolean; help: string; hideHelp: boolean; placeholder: string;
|
|
32
|
+
checkLabel: string; displayText: string;
|
|
33
|
+
value: string; dateText: string; hideDateValue: boolean; hideDatePlaceholder: boolean; checked: boolean;
|
|
34
|
+
error: string; noError: boolean;
|
|
35
|
+
/** The error line under a control with no error slot of its own (date, choice, checkbox). */
|
|
36
|
+
noErrorLine: boolean;
|
|
37
|
+
options: OptionRow[];
|
|
38
|
+
// hiddenWhen hides on TRUE: one "not" flag per field kind.
|
|
39
|
+
notText: boolean; notArea: boolean; notEmail: boolean; notPhone: boolean; notNumber: boolean; notUrl: boolean;
|
|
40
|
+
notDate: boolean; notCheck: boolean; notChoice: boolean; notMulti: boolean; notDisplay: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface S extends PresenceFlags {
|
|
44
|
+
status: "loading" | "ready";
|
|
45
|
+
title: string;
|
|
46
|
+
rows: FieldRow[];
|
|
47
|
+
pageCaption: string; noPageCaption: boolean;
|
|
48
|
+
primaryLabel: string; backLabel: string; hideSingle: boolean; hidePair: boolean;
|
|
49
|
+
sending: boolean;
|
|
50
|
+
sendError: string; noSendError: boolean;
|
|
51
|
+
/** The fields and the footer: hidden unless the form is there, fillable, open and not sent. */
|
|
52
|
+
hideForm: boolean;
|
|
53
|
+
hideUnsupported: boolean; hideClosed: boolean;
|
|
54
|
+
/** No site URL (hosted mode): the notice keeps its sentence and drops the link. */
|
|
55
|
+
hideOpenWebsite: boolean;
|
|
56
|
+
hideDone: boolean; doneTitle: string; doneBody: string;
|
|
57
|
+
hideOfflineError: boolean; hideServerError: boolean;
|
|
58
|
+
dateSheetOpen: boolean; dateSheetTitle: string;
|
|
59
|
+
/** The sheet's date (unix seconds) — the picker shows it and Done writes it. */
|
|
60
|
+
dateDraft: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const BASE: S = {
|
|
64
|
+
status: "loading", title: "", rows: [], pageCaption: "", noPageCaption: true,
|
|
65
|
+
primaryLabel: "", backLabel: "", hideSingle: true, hidePair: true, sending: false, sendError: "", noSendError: true,
|
|
66
|
+
hideForm: true, hideUnsupported: true, hideClosed: true, hideOpenWebsite: true, hideDone: true, doneTitle: "", doneBody: "",
|
|
67
|
+
hideOfflineError: true, hideServerError: true, dateSheetOpen: false, dateSheetTitle: "", dateDraft: 0,
|
|
68
|
+
...presence("loading"),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// The worker is the source of truth for what the visitor typed: a keystroke updates this
|
|
72
|
+
// map and returns the state unchanged, so typing never re-renders the field list.
|
|
73
|
+
let form: FormModel | undefined;
|
|
74
|
+
let values: Record<string, FieldValue> = {};
|
|
75
|
+
let page = 0;
|
|
76
|
+
let problems: Record<string, string> = {};
|
|
77
|
+
let siteUrl = "";
|
|
78
|
+
let dateTarget = "";
|
|
79
|
+
let dateDraft = 0;
|
|
80
|
+
/** Done writes only a date the visitor picked: Android's standalone DatePicker does not read
|
|
81
|
+
* `valuePath` and opens blank, so an untouched Done must not write a date nobody saw. */
|
|
82
|
+
let datePicked = false;
|
|
83
|
+
|
|
84
|
+
const valueOf = (target: string): FieldValue => values[target] ?? EMPTY_VALUE;
|
|
85
|
+
const pageFields = (): FormFieldModel[] => (form?.pages[page] ?? []).map((id) => form!.fields[id]).filter(Boolean);
|
|
86
|
+
const allFields = (): FormFieldModel[] => (form ? form.pages.flat().map((id) => form!.fields[id]).filter(Boolean) : []);
|
|
87
|
+
const lastPage = (): boolean => !form || page >= form.pages.length - 1;
|
|
88
|
+
|
|
89
|
+
function problemText(f: FormFieldModel, type: string): string {
|
|
90
|
+
switch (type) {
|
|
91
|
+
case "required":
|
|
92
|
+
return f.kind === "checkbox" ? copyParam("form.errorCheck", "Check this box to continue.")
|
|
93
|
+
: f.kind === "choice" || f.kind === "multi" ? copyParam("form.errorChoose", "Choose an option.")
|
|
94
|
+
: copyParam("form.errorRequired", "Enter a value.");
|
|
95
|
+
case "email": return copyParam("form.errorEmail", "Enter a valid email.");
|
|
96
|
+
case "number": return copyParam("form.errorNumber", "Enter a number.");
|
|
97
|
+
case "url": return copyParam("form.errorUrl", "Enter a valid link.");
|
|
98
|
+
case "REQUIRED_VALUE_ERROR": return problemText(f, "required");
|
|
99
|
+
case "FORMAT_ERROR":
|
|
100
|
+
return f.kind === "email" ? problemText(f, "email") : f.kind === "phone" ? copyParam("form.errorPhone", "Enter a valid phone number.")
|
|
101
|
+
: f.kind === "url" ? problemText(f, "url") : copyParam("form.errorInvalid", "Check this value.");
|
|
102
|
+
default: return copyParam("form.errorInvalid", "Check this value.");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function dateDisplay(iso: string): string {
|
|
107
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
|
|
108
|
+
return m ? formatDateMedium(new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]))) : iso;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function row(f: FormFieldModel): FieldRow {
|
|
112
|
+
const v = valueOf(f.target);
|
|
113
|
+
const error = problems[f.target] ? problemText(f, problems[f.target]) : "";
|
|
114
|
+
const marked = f.label ? `${f.label}${f.required ? " *" : ""}` : "";
|
|
115
|
+
// A checkbox's label sits beside its box, and a text block is its own text: neither has
|
|
116
|
+
// a label line above it.
|
|
117
|
+
const label = f.kind === "checkbox" || f.kind === "display" ? "" : marked;
|
|
118
|
+
return {
|
|
119
|
+
id: f.id, target: f.target,
|
|
120
|
+
label, hideLabel: !label, help: f.help, hideHelp: !f.help,
|
|
121
|
+
checkLabel: f.kind === "checkbox" ? marked : "", displayText: f.kind === "display" ? f.label : "",
|
|
122
|
+
placeholder: f.kind === "date" ? (f.placeholder || copyParam("form.datePlaceholder", "Choose a date")) : f.placeholder,
|
|
123
|
+
value: v.text, dateText: dateDisplay(v.text), hideDateValue: !v.text, hideDatePlaceholder: !!v.text, checked: v.checked,
|
|
124
|
+
error, noError: !error,
|
|
125
|
+
noErrorLine: !error || !(f.kind === "date" || f.kind === "checkbox" || f.kind === "choice" || f.kind === "multi"),
|
|
126
|
+
options: f.options.map((o, i) => ({ id: `${f.id}_${i}`, target: f.target, value: o.value, label: o.label, selected: v.selected.includes(o.value) })),
|
|
127
|
+
notText: f.kind !== "text", notArea: f.kind !== "area", notEmail: f.kind !== "email", notPhone: f.kind !== "phone",
|
|
128
|
+
notNumber: f.kind !== "number", notUrl: f.kind !== "url", notDate: f.kind !== "date", notCheck: f.kind !== "checkbox",
|
|
129
|
+
notChoice: f.kind !== "choice", notMulti: f.kind !== "multi", notDisplay: f.kind !== "display",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The visible state for the current form, page, values and problems. */
|
|
134
|
+
function view(s: S, patch: Partial<S> = {}): S {
|
|
135
|
+
if (!form) return { ...s, ...patch };
|
|
136
|
+
const pages = form.pages.length;
|
|
137
|
+
const fillable = !form.unsupported && !form.closed;
|
|
138
|
+
return {
|
|
139
|
+
...s,
|
|
140
|
+
status: "ready",
|
|
141
|
+
title: form.title || copyParam("form.untitled", "Untitled form"),
|
|
142
|
+
rows: fillable ? pageFields().map(row) : [],
|
|
143
|
+
pageCaption: pages > 1 ? copyTemplate("form.pageCaption", "Page {page} of {total}", { page: String(page + 1), total: String(pages) }) : "",
|
|
144
|
+
noPageCaption: !fillable || pages < 2,
|
|
145
|
+
primaryLabel: lastPage() ? (form.submitText || copyParam("form.submitCta", "Submit")) : (form.nextText || copyParam("form.nextCta", "Next")),
|
|
146
|
+
backLabel: form.backText || copyParam("form.previousCta", "Back"),
|
|
147
|
+
hideSingle: !fillable || page > 0,
|
|
148
|
+
hidePair: !fillable || page === 0,
|
|
149
|
+
hideForm: !fillable,
|
|
150
|
+
hideUnsupported: !form.unsupported,
|
|
151
|
+
hideOpenWebsite: !siteUrl,
|
|
152
|
+
hideClosed: form.unsupported || !form.closed,
|
|
153
|
+
hideDone: true,
|
|
154
|
+
hideOfflineError: true, hideServerError: true,
|
|
155
|
+
...presence("found"),
|
|
156
|
+
...patch,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** A signed-in member's own details by contact field, or `{}` for a visitor or a member read
|
|
161
|
+
* that fails. Fetched beside the form (not after it), so a member does not wait twice. */
|
|
162
|
+
async function memberDetails(): Promise<Record<string, string | undefined>> {
|
|
163
|
+
const access = await memberAccess().catch(() => ({ email: "", scope: "" }));
|
|
164
|
+
if (!access.email) return {};
|
|
165
|
+
let contact: { firstName?: string; lastName?: string; phones?: string[] } = {};
|
|
166
|
+
let loginEmail = access.email;
|
|
167
|
+
try {
|
|
168
|
+
const r = await getMyMember({ fieldsets: ["FULL"] }) as { member?: { loginEmail?: string; contact?: typeof contact } };
|
|
169
|
+
contact = r.member?.contact ?? {};
|
|
170
|
+
loginEmail = (r.member?.loginEmail || access.email).trim();
|
|
171
|
+
} catch { /* the email from the session is still the member's own */ }
|
|
172
|
+
return { EMAIL: loginEmail, FIRST_NAME: contact.firstName, LAST_NAME: contact.lastName, PHONE: contact.phones?.[0] };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The member's details, written into the empty fields they map to. */
|
|
176
|
+
function prefill(byField: Record<string, string | undefined>): void {
|
|
177
|
+
for (const f of allFields()) {
|
|
178
|
+
const known = byField[f.contactField]?.trim();
|
|
179
|
+
if (!known || valueOf(f.target).text) continue;
|
|
180
|
+
values[f.target] = { ...valueOf(f.target), text: known };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function setValue(target: string, next: Partial<FieldValue>): void {
|
|
185
|
+
values[target] = { ...valueOf(target), ...next };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function argOf(arg: unknown): Record<string, unknown> {
|
|
189
|
+
return arg && typeof arg === "object" ? arg as Record<string, unknown> : {};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function messageFor(err: unknown): Partial<S> {
|
|
193
|
+
const offline = (err as { code?: unknown })?.code === "offline_write_blocked" || classifyLoadError(err) === "offline";
|
|
194
|
+
return {
|
|
195
|
+
sending: false,
|
|
196
|
+
sendError: offline ? copyParam("form.offlineError", "Connect to the internet, then try again.") : copyParam("form.sendError", "Couldn't send the form. Try again."),
|
|
197
|
+
noSendError: false,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Keeps the problems that name a field the screen shows and moves to the first page with one;
|
|
202
|
+
* false when none does. A server error for a target the app does not render (a hidden field, an
|
|
203
|
+
* unknown path) cannot be shown in a field, so the caller says so by the button instead. */
|
|
204
|
+
function applyProblems(found: Record<string, string>): boolean {
|
|
205
|
+
const shown = new Set(allFields().map((f) => f.target));
|
|
206
|
+
problems = Object.fromEntries(Object.entries(found).filter(([target]) => shown.has(target)));
|
|
207
|
+
const targets = Object.keys(problems);
|
|
208
|
+
if (!targets.length || !form) return false;
|
|
209
|
+
const pageWithProblem = form.pages.findIndex((ids) => ids.some((id) => targets.includes(form!.fields[id]?.target ?? "")));
|
|
210
|
+
if (pageWithProblem >= 0) page = pageWithProblem;
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** A send in flight is numbered; a later load or send makes an earlier answer stale. */
|
|
215
|
+
let sendVersion = 0;
|
|
216
|
+
|
|
217
|
+
type SendRequest = Parameters<typeof createSubmission>[0];
|
|
218
|
+
|
|
219
|
+
/** Wix's own checks first: they answer per field and create nothing. A check that cannot
|
|
220
|
+
* run (offline, an outage) is not a reason to block the send. Then the send itself,
|
|
221
|
+
* opted into the offline outbox. */
|
|
222
|
+
async function send(version: number, request: SendRequest): Promise<void> {
|
|
223
|
+
let outcome: Record<string, unknown>;
|
|
224
|
+
try {
|
|
225
|
+
const check = await validateFormSubmission(request).catch(() => undefined);
|
|
226
|
+
const found = check && check.valid === false ? serverProblems(check) : {};
|
|
227
|
+
if (Object.keys(found).length) outcome = { outcome: "problems", problems: found };
|
|
228
|
+
else {
|
|
229
|
+
const sent = await createSubmission(request, { offline: "queue" });
|
|
230
|
+
outcome = { outcome: sent.status === "queued" ? "queued" : "sent" };
|
|
231
|
+
}
|
|
232
|
+
} catch (err) {
|
|
233
|
+
const found = serverProblems(err);
|
|
234
|
+
outcome = Object.keys(found).length
|
|
235
|
+
? { outcome: "problems", problems: found }
|
|
236
|
+
: { outcome: "failed", error: { code: (err as { code?: unknown })?.code, message: String((err as Error)?.message ?? err) } };
|
|
237
|
+
}
|
|
238
|
+
await svc.finishSend({ version, ...outcome });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** The form was sent (or queued): its done state outlives a repeat onAppear load. */
|
|
242
|
+
let completed = false;
|
|
243
|
+
let inflight: Promise<S> | undefined;
|
|
244
|
+
let inflightId = "";
|
|
245
|
+
|
|
246
|
+
async function loadForm(id: string): Promise<S> {
|
|
247
|
+
form = undefined; values = {}; page = 0; problems = {}; sendVersion += 1; completed = false;
|
|
248
|
+
if (!id) return { ...BASE, status: "ready", ...presence("missing") };
|
|
249
|
+
const member = memberDetails();
|
|
250
|
+
try {
|
|
251
|
+
const res = await getForm(id);
|
|
252
|
+
const parsed = parseForm(res);
|
|
253
|
+
if (!parsed.id || (res as { form?: { enabled?: boolean } }).form?.enabled === false) return { ...BASE, status: "ready", ...presence("missing") };
|
|
254
|
+
form = parsed;
|
|
255
|
+
prefill(await member);
|
|
256
|
+
return view({ ...BASE });
|
|
257
|
+
} catch (err) {
|
|
258
|
+
const p = presenceAfterError(err);
|
|
259
|
+
if (p.notFound) return { ...BASE, status: "ready", ...p };
|
|
260
|
+
const offline = classifyLoadError(err) === "offline";
|
|
261
|
+
return { ...BASE, status: "ready", ...p, hideLoadError: true, hideOfflineError: !offline, hideServerError: offline };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const svc = defineService<S, {
|
|
266
|
+
load: (s: S, arg?: unknown) => Promise<S>;
|
|
267
|
+
setText: (s: S, arg?: unknown) => S;
|
|
268
|
+
toggleCheck: (s: S, arg?: unknown) => S;
|
|
269
|
+
choose: (s: S, arg?: unknown) => S;
|
|
270
|
+
toggleOption: (s: S, arg?: unknown) => S;
|
|
271
|
+
openDate: (s: S, arg?: unknown) => S;
|
|
272
|
+
draftDate: (s: S, arg?: unknown) => S;
|
|
273
|
+
applyDate: (s: S) => S;
|
|
274
|
+
closeDate: (s: S) => S;
|
|
275
|
+
next: (s: S) => S;
|
|
276
|
+
finishSend: (s: S, arg?: unknown) => S;
|
|
277
|
+
back: (s: S) => S;
|
|
278
|
+
openWebsite: (s: S) => Promise<S>;
|
|
279
|
+
}>("himiBrandedForm", {
|
|
280
|
+
state: BASE,
|
|
281
|
+
actions: {
|
|
282
|
+
load: async (s, arg) => {
|
|
283
|
+
stashCopy(arg);
|
|
284
|
+
const a = argOf(arg);
|
|
285
|
+
if (typeof a.siteUrl === "string") siteUrl = a.siteUrl.trim();
|
|
286
|
+
const id = detailInputId();
|
|
287
|
+
// The boot load and the descriptor's onAppear both ask for the same form, and onAppear fires
|
|
288
|
+
// again when the screen comes back (a sheet or the in-app browser closes). The same form id
|
|
289
|
+
// keeps what the visitor typed and the page they are on; only a new id, or a retry after a
|
|
290
|
+
// failure (no form held), starts over.
|
|
291
|
+
// A form already sent keeps its thank-you state: re-opening the editable form would invite
|
|
292
|
+
// a second submission.
|
|
293
|
+
if (form && id && form.id === id) return completed ? s : view(s);
|
|
294
|
+
if (inflight && inflightId === id) return inflight;
|
|
295
|
+
inflightId = id;
|
|
296
|
+
inflight = loadForm(id);
|
|
297
|
+
try { return await inflight; } finally { inflight = undefined; }
|
|
298
|
+
},
|
|
299
|
+
// Keystrokes: the value lives in the worker; the state only changes to clear an error.
|
|
300
|
+
setText: (s, arg) => {
|
|
301
|
+
const a = argOf(arg);
|
|
302
|
+
const target = String(a.target ?? "");
|
|
303
|
+
const text = typeof a.value === "string" ? a.value : typeof a.text === "string" ? a.text : "";
|
|
304
|
+
if (!target) return s;
|
|
305
|
+
setValue(target, { text });
|
|
306
|
+
if (!problems[target]) return s;
|
|
307
|
+
delete problems[target];
|
|
308
|
+
return view(s);
|
|
309
|
+
},
|
|
310
|
+
toggleCheck: (s, arg) => {
|
|
311
|
+
const a = argOf(arg);
|
|
312
|
+
const target = String(a.target ?? "");
|
|
313
|
+
if (!target) return s;
|
|
314
|
+
setValue(target, { checked: typeof a.value === "boolean" ? a.value : !valueOf(target).checked });
|
|
315
|
+
delete problems[target];
|
|
316
|
+
return view(s);
|
|
317
|
+
},
|
|
318
|
+
choose: (s, arg) => {
|
|
319
|
+
const a = argOf(arg);
|
|
320
|
+
const target = String(a.target ?? ""), value = String(a.option ?? "");
|
|
321
|
+
if (!target || !value) return s;
|
|
322
|
+
setValue(target, { selected: [value] });
|
|
323
|
+
delete problems[target];
|
|
324
|
+
return view(s);
|
|
325
|
+
},
|
|
326
|
+
toggleOption: (s, arg) => {
|
|
327
|
+
const a = argOf(arg);
|
|
328
|
+
const target = String(a.target ?? ""), value = String(a.option ?? "");
|
|
329
|
+
if (!target || !value) return s;
|
|
330
|
+
const sel = valueOf(target).selected;
|
|
331
|
+
setValue(target, { selected: sel.includes(value) ? sel.filter((x) => x !== value) : [...sel, value] });
|
|
332
|
+
delete problems[target];
|
|
333
|
+
return view(s);
|
|
334
|
+
},
|
|
335
|
+
openDate: (s, arg) => {
|
|
336
|
+
const target = String(argOf(arg).target ?? "");
|
|
337
|
+
const f = allFields().find((x) => x.target === target);
|
|
338
|
+
if (!f || s.sending) return s;
|
|
339
|
+
dateTarget = target;
|
|
340
|
+
datePicked = false;
|
|
341
|
+
const iso = valueOf(target).text;
|
|
342
|
+
dateDraft = /^\d{4}-\d{2}-\d{2}$/.test(iso) ? Math.floor(new Date(`${iso}T12:00:00`).getTime() / 1000) : Math.floor(Date.now() / 1000);
|
|
343
|
+
return { ...s, dateSheetOpen: true, dateSheetTitle: f.label, dateDraft };
|
|
344
|
+
},
|
|
345
|
+
draftDate: (s, arg) => {
|
|
346
|
+
const v = Number(argOf(arg).value);
|
|
347
|
+
if (!Number.isFinite(v) || v <= 0) return s;
|
|
348
|
+
dateDraft = v;
|
|
349
|
+
datePicked = true;
|
|
350
|
+
return { ...s, dateDraft };
|
|
351
|
+
},
|
|
352
|
+
applyDate: (s) => {
|
|
353
|
+
if (dateTarget && dateDraft && datePicked) { setValue(dateTarget, { text: isoDate(dateDraft) }); delete problems[dateTarget]; }
|
|
354
|
+
return view({ ...s, dateSheetOpen: false });
|
|
355
|
+
},
|
|
356
|
+
closeDate: (s) => ({ ...s, dateSheetOpen: false }),
|
|
357
|
+
back: (s) => {
|
|
358
|
+
if (page === 0 || s.sending) return s;
|
|
359
|
+
page -= 1;
|
|
360
|
+
return view(s, { noSendError: true, sendError: "" });
|
|
361
|
+
},
|
|
362
|
+
// "Next" on an earlier page, "Submit" on the last one.
|
|
363
|
+
next: (s) => {
|
|
364
|
+
if (!form || s.sending) return s;
|
|
365
|
+
const local = localProblems(pageFields(), values);
|
|
366
|
+
if (applyProblems(local)) return view(s, { noSendError: true, sendError: "" });
|
|
367
|
+
if (!lastPage()) { page += 1; problems = {}; return view(s, { noSendError: true, sendError: "" }); }
|
|
368
|
+
const all = allFields();
|
|
369
|
+
const allLocal = localProblems(all, values);
|
|
370
|
+
if (applyProblems(allLocal)) return view(s, { noSendError: true, sendError: "" });
|
|
371
|
+
const version = ++sendVersion;
|
|
372
|
+
void send(version, { submission: { formId: form.id, submissions: submissionValues(all, values) } });
|
|
373
|
+
return { ...s, sending: true, noSendError: true, sendError: "" };
|
|
374
|
+
},
|
|
375
|
+
finishSend: (s, arg) => {
|
|
376
|
+
const r = argOf(arg) as { version?: unknown; outcome?: unknown; problems?: unknown; error?: unknown };
|
|
377
|
+
if (r.version !== sendVersion || !form) return s;
|
|
378
|
+
if (r.outcome === "problems") {
|
|
379
|
+
if (applyProblems(r.problems as Record<string, string>)) return view(s, { sending: false, noSendError: true, sendError: "" });
|
|
380
|
+
return view(s, { sending: false, noSendError: false, sendError: copyParam("form.checkError", "Some answers can't be sent. Check the form and try again.") });
|
|
381
|
+
}
|
|
382
|
+
if (r.outcome === "failed") return view(s, messageFor(r.error));
|
|
383
|
+
const queued = r.outcome === "queued";
|
|
384
|
+
completed = true;
|
|
385
|
+
return view(s, {
|
|
386
|
+
sending: false, hideForm: true, hideSingle: true, hidePair: true, hideDone: false, rows: [], noPageCaption: true,
|
|
387
|
+
doneTitle: queued ? copyParam("form.queuedTitle", "Form saved") : copyParam("form.sentTitle", "Thank you"),
|
|
388
|
+
doneBody: queued ? copyParam("form.queuedBody", "We'll send your form when you're back online.")
|
|
389
|
+
: (form.thankYou || copyParam("form.sentBody", "Thanks, we received your submission.")),
|
|
390
|
+
});
|
|
391
|
+
},
|
|
392
|
+
openWebsite: async (s) => {
|
|
393
|
+
if (siteUrl) { try { await openUrl(siteUrl, "in_app_browser"); } catch { /* the handoff is best-effort */ } }
|
|
394
|
+
return s;
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
svc.load();
|
|
399
|
+
export { svc };
|
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
// visitor) and an empty one is a real empty state — never sample rows presented as
|
|
6
6
|
// the customer's own. Samples appear only in the host's explicit sample preview
|
|
7
7
|
// (_shared/branded-load-state.ts).
|
|
8
|
+
import { classifyLoadError } from "@himalaya/state";
|
|
8
9
|
import { defineService } from "../_shared/collection-service.js";
|
|
9
10
|
import { LOADING, loaded, loadFailed, type LoadCopy, type LoadFlags } from "../_shared/branded-load-state.js";
|
|
11
|
+
import { copyParam, stashCopy } from "../_shared/himi-copy.js";
|
|
12
|
+
import { hasInputFields } from "../_shared/branded-form.js";
|
|
10
13
|
import { queryForms, type WixForm } from "@himalaya/wix-forms";
|
|
11
14
|
|
|
12
15
|
interface Row { id: string; title: string; subtitle: string }
|
|
@@ -17,6 +20,9 @@ interface S extends LoadFlags {
|
|
|
17
20
|
* query failed. A failed query must never render the sentence that claims the
|
|
18
21
|
* customer has no content. */
|
|
19
22
|
notMockEmpty: boolean; notMockUnavailable: boolean;
|
|
23
|
+
/** The list's own states (the screen has no collection chrome): EmptyState only for a
|
|
24
|
+
* real empty answer, one ErrorState per failure class. */
|
|
25
|
+
noEmpty: boolean; hideOfflineError: boolean; hideServerError: boolean; hideSignInError: boolean;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
/** The Wix Forms namespace the Forms app owns. Booking/event registration forms live
|
|
@@ -24,7 +30,7 @@ interface S extends LoadFlags {
|
|
|
24
30
|
const FORMS_NAMESPACE = "wix.form_app.form";
|
|
25
31
|
|
|
26
32
|
function norm(f: WixForm): Row {
|
|
27
|
-
const title = (f.name || "
|
|
33
|
+
const title = (f.name || "").trim() || copyParam("forms.untitled", "Untitled form");
|
|
28
34
|
// The subtitle used to be "5 fields · Published" — a form-builder metric and a publishing
|
|
29
35
|
// status, both back-office facts ([branded-lite-qa] #3077). A visitor sees the form's own
|
|
30
36
|
// description when it has one, and nothing otherwise.
|
|
@@ -49,18 +55,27 @@ const COPY: LoadCopy<Row> = {
|
|
|
49
55
|
sample: () => MOCK.map((r) => ({ ...r })),
|
|
50
56
|
};
|
|
51
57
|
|
|
52
|
-
const
|
|
53
|
-
|
|
58
|
+
const NO_STATES = { noEmpty: true, hideOfflineError: true, hideServerError: true, hideSignInError: true };
|
|
59
|
+
|
|
60
|
+
const svc = defineService<S, { load: (s: S, arg?: unknown) => Promise<S> }>("himiBrandedForms", {
|
|
61
|
+
state: { ...LOADING, forms: [], emptyText: "", ...NO_STATES },
|
|
54
62
|
actions: {
|
|
55
|
-
load: async (s) => {
|
|
63
|
+
load: async (s, arg) => {
|
|
64
|
+
stashCopy(arg);
|
|
56
65
|
try {
|
|
57
66
|
const r = await queryForms({ filter: { namespace: FORMS_NAMESPACE }, paging: { limit: 50 } });
|
|
58
67
|
// A visitor can only fill a live form: a disabled or draft one used to be listed with a
|
|
59
68
|
// "Disabled"/"Draft" label; now that the row carries no back-office status, it is not listed.
|
|
60
|
-
const forms = (r.forms ?? [])
|
|
61
|
-
|
|
69
|
+
const forms = (r.forms ?? [])
|
|
70
|
+
.filter((f) => f.enabled !== false && !/^DRAFT$/i.test(String(f.status ?? "")) && hasInputFields(f))
|
|
71
|
+
.map(norm);
|
|
72
|
+
const next = loaded("forms", forms, COPY);
|
|
73
|
+
return { ...s, ...next, ...NO_STATES, noEmpty: next.forms.length > 0 || next.isMock };
|
|
62
74
|
} catch (err) {
|
|
63
|
-
|
|
75
|
+
const next = loadFailed("forms", err, COPY);
|
|
76
|
+
// One state per class: offline and server failures offer Retry; a refused visitor offers Sign in.
|
|
77
|
+
const kind = next.signInRequired ? "signIn" : classifyLoadError(err) === "offline" ? "offline" : "server";
|
|
78
|
+
return { ...s, ...next, ...NO_STATES, hideOfflineError: next.isMock || kind !== "offline", hideServerError: next.isMock || kind !== "server", hideSignInError: next.isMock || kind !== "signIn" };
|
|
64
79
|
}
|
|
65
80
|
},
|
|
66
81
|
},
|
|
@@ -481,7 +481,7 @@ function applePurchase(state: S): IapPurchaseFields {
|
|
|
481
481
|
const id = inputId();
|
|
482
482
|
const loaded = !!id && lastLoadedId === id;
|
|
483
483
|
const sellable = SELLS_THROUGH_APPLE && loaded && lastProduct?.productType?.toLowerCase() === "digital" && !state.optionsUnknown && state.optionGroups.length === 0;
|
|
484
|
-
const copy =
|
|
484
|
+
const copy = iapPurchaseCopy("product");
|
|
485
485
|
const fields = sellable ? productPricing.purchaseFields({ kind: "storesProduct", id }, copy) : NOT_APPLE;
|
|
486
486
|
return fields.appleRoute ? fields : NOT_APPLE;
|
|
487
487
|
}
|
|
@@ -38,6 +38,19 @@ export default defineApp({
|
|
|
38
38
|
sync: "http://localhost:8190/v1/sync",
|
|
39
39
|
},
|
|
40
40
|
capabilities: ["live_activity","push","share","secure_storage","open_url","calendar","local_database","wix_api","wix_platform","app_info","iap","release_control","clipboard","theme"],
|
|
41
|
+
cache: {
|
|
42
|
+
"rules": [
|
|
43
|
+
{
|
|
44
|
+
"method": "POST",
|
|
45
|
+
"pathPrefix": "/form-submission-service/v4/submissions",
|
|
46
|
+
"cacheable": false,
|
|
47
|
+
"ttl": 0,
|
|
48
|
+
"swr": 0,
|
|
49
|
+
"tag": "forms",
|
|
50
|
+
"queueOffline": true
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
41
54
|
designTokensPath: "./tokens.json",
|
|
42
55
|
navigation: {
|
|
43
56
|
"navKind": "responsive",
|
|
@@ -111,7 +111,7 @@ export function createAttendancePurchase(options: { kind: "bookingsService" | "e
|
|
|
111
111
|
const classified = classifyForApple(metadata, currency);
|
|
112
112
|
const candidate = platform === "ios" && classified === "appleIap";
|
|
113
113
|
const price = iapPriceFields(commerce.pricing.view(reference), iapPriceCopy(prefix));
|
|
114
|
-
const copy =
|
|
114
|
+
const copy = iapPurchaseCopy(prefix);
|
|
115
115
|
const purchase = commerce.purchaseFields(reference, copy);
|
|
116
116
|
const apple = candidate || (platform === "ios" && classified !== "wixPayments" && purchase.appleRoute);
|
|
117
117
|
const enabled = attendanceCheckoutRoute(metadata, currency, platform) === "appleIap";
|
|
@@ -20,7 +20,7 @@ export function iapPriceCopy(prefix: string): IapPriceCopy {
|
|
|
20
20
|
* `iapPurchaseFields`. Customer copy only: no backend names, ids or diagnostics. */
|
|
21
21
|
export function iapPurchaseCopy(prefix: string): IapPurchaseCopy {
|
|
22
22
|
return {
|
|
23
|
-
buy: copyParam(`${prefix}.buyCta`, "
|
|
23
|
+
buy: copyParam(`${prefix}.buyCta`, "Buy now"),
|
|
24
24
|
processing: copyParam(`${prefix}.purchaseProcessingText`, "Processing your purchase…"),
|
|
25
25
|
awaitingApproval: copyParam(`${prefix}.purchaseAwaitingApprovalText`, "Waiting for approval"),
|
|
26
26
|
unavailable: copyParam(`${prefix}.purchaseTemporarilyUnavailableText`, "Purchasing is temporarily unavailable."),
|
|
@@ -38,6 +38,19 @@ export default defineApp({
|
|
|
38
38
|
sync: "http://localhost:8190/v1/sync",
|
|
39
39
|
},
|
|
40
40
|
capabilities: ["live_activity","push","share","secure_storage","open_url","calendar","local_database","wix_api","wix_platform","app_info","iap","release_control","clipboard","theme"],
|
|
41
|
+
cache: {
|
|
42
|
+
"rules": [
|
|
43
|
+
{
|
|
44
|
+
"method": "POST",
|
|
45
|
+
"pathPrefix": "/form-submission-service/v4/submissions",
|
|
46
|
+
"cacheable": false,
|
|
47
|
+
"ttl": 0,
|
|
48
|
+
"swr": 0,
|
|
49
|
+
"tag": "forms",
|
|
50
|
+
"queueOffline": true
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
41
54
|
designTokensPath: "./tokens.json",
|
|
42
55
|
navigation: {
|
|
43
56
|
"navKind": "responsive",
|
|
@@ -111,7 +111,7 @@ export function createAttendancePurchase(options: { kind: "bookingsService" | "e
|
|
|
111
111
|
const classified = classifyForApple(metadata, currency);
|
|
112
112
|
const candidate = platform === "ios" && classified === "appleIap";
|
|
113
113
|
const price = iapPriceFields(commerce.pricing.view(reference), iapPriceCopy(prefix));
|
|
114
|
-
const copy =
|
|
114
|
+
const copy = iapPurchaseCopy(prefix);
|
|
115
115
|
const purchase = commerce.purchaseFields(reference, copy);
|
|
116
116
|
const apple = candidate || (platform === "ios" && classified !== "wixPayments" && purchase.appleRoute);
|
|
117
117
|
const enabled = attendanceCheckoutRoute(metadata, currency, platform) === "appleIap";
|
|
@@ -20,7 +20,7 @@ export function iapPriceCopy(prefix: string): IapPriceCopy {
|
|
|
20
20
|
* `iapPurchaseFields`. Customer copy only: no backend names, ids or diagnostics. */
|
|
21
21
|
export function iapPurchaseCopy(prefix: string): IapPurchaseCopy {
|
|
22
22
|
return {
|
|
23
|
-
buy: copyParam(`${prefix}.buyCta`, "
|
|
23
|
+
buy: copyParam(`${prefix}.buyCta`, "Buy now"),
|
|
24
24
|
processing: copyParam(`${prefix}.purchaseProcessingText`, "Processing your purchase…"),
|
|
25
25
|
awaitingApproval: copyParam(`${prefix}.purchaseAwaitingApprovalText`, "Waiting for approval"),
|
|
26
26
|
unavailable: copyParam(`${prefix}.purchaseTemporarilyUnavailableText`, "Purchasing is temporarily unavailable."),
|
|
@@ -38,6 +38,19 @@ export default defineApp({
|
|
|
38
38
|
sync: "http://localhost:8190/v1/sync",
|
|
39
39
|
},
|
|
40
40
|
capabilities: ["live_activity","push","share","secure_storage","open_url","calendar","local_database","wix_api","wix_platform","app_info","iap","release_control","clipboard","motion","theme"],
|
|
41
|
+
cache: {
|
|
42
|
+
"rules": [
|
|
43
|
+
{
|
|
44
|
+
"method": "POST",
|
|
45
|
+
"pathPrefix": "/form-submission-service/v4/submissions",
|
|
46
|
+
"cacheable": false,
|
|
47
|
+
"ttl": 0,
|
|
48
|
+
"swr": 0,
|
|
49
|
+
"tag": "forms",
|
|
50
|
+
"queueOffline": true
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
41
54
|
designTokensPath: "./tokens.json",
|
|
42
55
|
navigation: {
|
|
43
56
|
"navKind": "responsive",
|
|
@@ -111,7 +111,7 @@ export function createAttendancePurchase(options: { kind: "bookingsService" | "e
|
|
|
111
111
|
const classified = classifyForApple(metadata, currency);
|
|
112
112
|
const candidate = platform === "ios" && classified === "appleIap";
|
|
113
113
|
const price = iapPriceFields(commerce.pricing.view(reference), iapPriceCopy(prefix));
|
|
114
|
-
const copy =
|
|
114
|
+
const copy = iapPurchaseCopy(prefix);
|
|
115
115
|
const purchase = commerce.purchaseFields(reference, copy);
|
|
116
116
|
const apple = candidate || (platform === "ios" && classified !== "wixPayments" && purchase.appleRoute);
|
|
117
117
|
const enabled = attendanceCheckoutRoute(metadata, currency, platform) === "appleIap";
|
|
@@ -20,7 +20,7 @@ export function iapPriceCopy(prefix: string): IapPriceCopy {
|
|
|
20
20
|
* `iapPurchaseFields`. Customer copy only: no backend names, ids or diagnostics. */
|
|
21
21
|
export function iapPurchaseCopy(prefix: string): IapPurchaseCopy {
|
|
22
22
|
return {
|
|
23
|
-
buy: copyParam(`${prefix}.buyCta`, "
|
|
23
|
+
buy: copyParam(`${prefix}.buyCta`, "Buy now"),
|
|
24
24
|
processing: copyParam(`${prefix}.purchaseProcessingText`, "Processing your purchase…"),
|
|
25
25
|
awaitingApproval: copyParam(`${prefix}.purchaseAwaitingApprovalText`, "Waiting for approval"),
|
|
26
26
|
unavailable: copyParam(`${prefix}.purchaseTemporarilyUnavailableText`, "Purchasing is temporarily unavailable."),
|
|
@@ -490,7 +490,7 @@ function applePurchase(state: S): IapPurchaseFields {
|
|
|
490
490
|
const id = inputId();
|
|
491
491
|
const loaded = !!id && lastLoadedId === id;
|
|
492
492
|
const sellable = SELLS_THROUGH_APPLE && loaded && lastProduct?.productType?.toLowerCase() === "digital" && !state.optionsUnknown && state.optionGroups.length === 0;
|
|
493
|
-
const copy =
|
|
493
|
+
const copy = iapPurchaseCopy("product");
|
|
494
494
|
const fields = sellable ? productPricing.purchaseFields({ kind: "storesProduct", id }, copy) : NOT_APPLE;
|
|
495
495
|
return fields.appleRoute ? fields : NOT_APPLE;
|
|
496
496
|
}
|
|
@@ -38,6 +38,19 @@ export default defineApp({
|
|
|
38
38
|
sync: "http://localhost:8190/v1/sync",
|
|
39
39
|
},
|
|
40
40
|
capabilities: ["live_activity","push","share","secure_storage","open_url","calendar","local_database","wix_api","wix_platform","app_info","iap","release_control","clipboard","theme"],
|
|
41
|
+
cache: {
|
|
42
|
+
"rules": [
|
|
43
|
+
{
|
|
44
|
+
"method": "POST",
|
|
45
|
+
"pathPrefix": "/form-submission-service/v4/submissions",
|
|
46
|
+
"cacheable": false,
|
|
47
|
+
"ttl": 0,
|
|
48
|
+
"swr": 0,
|
|
49
|
+
"tag": "forms",
|
|
50
|
+
"queueOffline": true
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
41
54
|
designTokensPath: "./tokens.json",
|
|
42
55
|
navigation: {
|
|
43
56
|
"navKind": "responsive",
|
|
@@ -111,7 +111,7 @@ export function createAttendancePurchase(options: { kind: "bookingsService" | "e
|
|
|
111
111
|
const classified = classifyForApple(metadata, currency);
|
|
112
112
|
const candidate = platform === "ios" && classified === "appleIap";
|
|
113
113
|
const price = iapPriceFields(commerce.pricing.view(reference), iapPriceCopy(prefix));
|
|
114
|
-
const copy =
|
|
114
|
+
const copy = iapPurchaseCopy(prefix);
|
|
115
115
|
const purchase = commerce.purchaseFields(reference, copy);
|
|
116
116
|
const apple = candidate || (platform === "ios" && classified !== "wixPayments" && purchase.appleRoute);
|
|
117
117
|
const enabled = attendanceCheckoutRoute(metadata, currency, platform) === "appleIap";
|