14 "js/wix/campaigns-store.js": "// Declarations of campaigns-store.ts: what this file exports, their arguments and what they return.\n// import type { CampaignSummary } from \"./donations-types.js\";\n// export interface CampaignsStoreOptions {\n// initialCampaigns?: CampaignSummary[];\n// }\n// export interface CampaignsState {\n// campaigns: CampaignSummary[] | null;\n// error: string | null;\n// }\n// export interface CampaignsStore {\n// getState(): CampaignsState;\n// subscribe(listener: (state: CampaignsState) => void): () => void;\n// start(): void;\n// stop(): void;\n// }\n// export declare function createCampaignsStore({ initialCampaigns }?: CampaignsStoreOptions): CampaignsStore;\n// The campaigns listing as a framework-free store — the logic behind useCampaigns, usable from\n// React (useCampaigns wraps it with useSyncExternalStore), from a static page or Vue/Svelte\n// (subscribe and render), or as the specification for a port.\n//\n// SSR-friendly: seed with `initialCampaigns` and they render at once with no client fetch; without\n// a seed `start()` fetches the non-archived campaigns. A failed load leaves `campaigns: []` with the\n// message in `error`, so the surface renders its empty state and the message, never a skeleton\n// forever. One store per mounted listing: createCampaignsStore(), not a singleton.\nimport { fetchCampaigns } from \"./campaigns.js\";\nexport function createCampaignsStore({ initialCampaigns } = {}) {\n let state = { campaigns: initialCampaigns ?? null, error: null };\n let started = false;\n const listeners = new Set();\n function setState(patch) {\n state = { ...state, ...patch };\n for (const fn of listeners)\n fn();\n }\n return {\n getState: () => state,\n subscribe(fn) {\n const listener = () => { try {\n fn(state);\n }\n catch (e) {\n console.error(\"campaigns-store listener failed\", e);\n } };\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n start() {\n if (started)\n return;\n started = true;\n if (initialCampaigns)\n return;\n fetchCampaigns()\n .then((campaigns) => { if (started)\n setState({ campaigns }); })\n .catch((e) => { if (started)\n setState({ campaigns: [], error: e instanceof Error ? e.message : String(e) }); });\n },\n stop() {\n started = false;\n },\n };\n}\n",
15 "js/wix/campaigns.js": "// Declarations of campaigns.ts: what this file exports, their arguments and what they return.\n// import type { CampaignDetail, CampaignSummary } from \"./donations-types.js\";\n// export declare function fetchCampaigns({ limit }?: {\n// limit?: number | undefined;\n// }): Promise<CampaignSummary[]>;\n// export declare function fetchCampaign(campaignId: string): Promise<CampaignDetail | null>;\n// export declare function fetchDefaultCampaign(): Promise<CampaignDetail | null>;\n// Campaign reads over REST — the twin of app/wix/donations/campaigns.ts. Same exports, same DTOs;\n// the rules and mappers come from donations-core (the SAME file the SDK transport uses, deployed\n// flat next to this one by deploy.mjs --stack static), so this file is only the transport: one\n// fetch with a literal body per function. Every call here runs with the visitor token; the metrics\n// read is guarded (a refusal leaves the raised total and the currency \"\").\n// Porting: keep the path, keep the body, port the core once.\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/donations/donation-campaigns/query-donation-campaigns.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/donations/donation-campaigns/get-donation-campaign-metrics.md\nimport { WixApiError, wixRequest } from \"./client.js\";\nimport { imgSrc } from \"./media.js\";\nimport { campaignsQuery, currencyOf, isArchived, rawId, toDetail, toSummary } from \"./donations-core.js\";\nconst BASE = \"/donation-campaigns/v2/donation-campaigns\";\n// A campaign nobody has donated to yet has no metrics entry, and the entity carries no currency code:\n// the site's currency then comes from the eCommerce settings, read once.\n// GET /_api/ecommerce-settings/v1/ecommerce-settings?fields=BUSINESS_INFO → { ecommerceSettings: { businessInfo: { currency } } }\nlet siteCurrencyPromise = null;\nfunction siteCurrency() {\n siteCurrencyPromise ??= wixRequest(\"/_api/ecommerce-settings/v1/ecommerce-settings\", { method: \"GET\", query: { fields: [\"BUSINESS_INFO\"] } })\n .then((res) => String(res?.ecommerceSettings?.businessInfo?.currency ?? \"\"))\n .catch(() => { siteCurrencyPromise = null; return \"\"; });\n return siteCurrencyPromise;\n}\n/** The campaign's currency: the metrics' when a donation exists, else the site's. */\nasync function currencyFor(metrics) {\n return currencyOf(metrics) || (await siteCurrency());\n}\n// GET /donation-campaigns/v2/donation-campaigns/{id}/metrics → { currencyMetricsList: [{ currencyCode, donationCount, totalAmount }] }\nasync function metricsOf(campaignId) {\n try {\n const res = await wixRequest(`${BASE}/${campaignId}/metrics`, { method: \"GET\" });\n return (res?.currencyMetricsList ?? []);\n }\n catch {\n return null; // unavailable is not a successfully-read zero total\n }\n}\n/**\n * The non-archived campaigns, oldest first, as card-ready DTOs.\n * POST /donation-campaigns/v2/donation-campaigns/query { query: { filter: { archived: false }, sort: [{ fieldName: \"createdDate\", order: \"ASC\" }], cursorPaging: { limit } } }\n */\nexport async function fetchCampaigns({ limit = 100 } = {}) {\n const res = await wixRequest(`${BASE}/query`, { body: { query: campaignsQuery({ limit }) } });\n const items = (res?.donationCampaigns ?? []).filter((raw) => !isArchived(raw));\n const metrics = await Promise.all(items.map((raw) => (raw.campaignGoal ? metricsOf(rawId(raw)) : Promise.resolve([]))));\n const currencies = await Promise.all(metrics.map(currencyFor));\n return items.map((raw, i) => toSummary(raw, metrics[i], currencies[i], imgSrc));\n}\n/**\n * One campaign by id with its goal progress and form options; null when not found (404) or archived.\n * GET /donation-campaigns/v2/donation-campaigns/{id} → { donationCampaign }\n */\nexport async function fetchCampaign(campaignId) {\n let raw;\n try {\n raw = (await wixRequest(`${BASE}/${campaignId}`, { method: \"GET\" }))?.donationCampaign;\n }\n catch (e) {\n if (e instanceof WixApiError && e.status === 404)\n return null;\n throw e;\n }\n if (!raw || isArchived(raw))\n return null; // an archived campaign is reachable by id but never shows a form\n const metrics = raw.campaignGoal ? await metricsOf(campaignId) : [];\n return toDetail(raw, metrics, await currencyFor(metrics), imgSrc);\n}\n/** The Wix widget's default: the first campaign by creation date. Null when the site has none. */\nexport async function fetchDefaultCampaign() {\n const [first] = await fetchCampaigns({ limit: 1 });\n return first ? fetchCampaign(first.id) : null;\n}\n",
16 "js/wix/donate.js": "// Declarations of donate.ts: what this file exports, their arguments and what they return.\n// import { type DonatePaths } from \"./donations-core.js\";\n// import type { DonationInput, DonationReceipt } from \"./donations-types.js\";\n// export type { DonatePaths };\n// export interface DonateOptions {\n// origin?: string;\n// paths?: DonatePaths;\n// }\n// export declare function donationCheckoutUrl(campaignId: string, input: DonationInput, { origin, paths }?: DonateOptions): Promise<string>;\n// export declare function fetchDonationReceipt(orderId: string): Promise<DonationReceipt | null>;\n// The donation flow over REST — the twin of app/wix/donations/donate.ts. Same exports, same\n// signatures; the bodies and result readers come from donations-core (the SAME file the SDK\n// transport uses). A donation is an eCom cart with one catalog line; the hosted checkout takes\n// payment and donor details. In Cart V2 the cart IS the checkout — its id is the id the redirect\n// session takes, so there is one route and no separate Create Checkout call. Never create the\n// order yourself, never hand-build a checkout URL.\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/create-cart.md\n// docs: https://dev.wix.com/docs/api-reference/business-management/headless/redirects/create-redirect-session.md\n// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/orders/orders/get-order.md\nimport { wixRequest } from \"./client.js\";\nimport { cartBody, cartIdOf, checkoutError, redirectBody, redirectUrl, toReceipt, } from \"./donations-core.js\";\n/**\n * Start the hosted checkout for a donation; resolves to the URL to navigate the FULL document to.\n * `origin` must be the site's real https origin as registered on the OAuth app's allowed domains.\n * POST /ecom/v2/carts { cart: { source: { channelType: \"WEB\" }, note? }, catalogItems: [{ quantity: 1, catalogReference: { appId, catalogItemId, options: { amount, frequency, donorCoveringFees? } } }] } → { cart: { id } } (the cart is the checkout)\n * POST /headless/v1/redirect-session { ecomCheckout: { checkoutId }, callbacks: { postFlowUrl, thankYouPageUrl } } → { redirectSession: { fullUrl } }\n */\nexport async function donationCheckoutUrl(campaignId, input, { origin, paths } = {}) {\n const site = origin ?? (typeof window !== \"undefined\" ? window.location.origin : \"\");\n let cartId;\n try {\n const res = await wixRequest(\"/ecom/v2/carts\", { body: cartBody(campaignId, input) });\n cartId = cartIdOf(res);\n }\n catch (e) {\n throw checkoutError(e, \"cart\");\n }\n try {\n const session = await wixRequest(\"/headless/v1/redirect-session\", { body: redirectBody(cartId, campaignId, site, paths) });\n return redirectUrl(session);\n }\n catch (e) {\n throw checkoutError(e, \"redirect\", cartId);\n }\n}\n/**\n * The order behind a completed donation, for the thank-you page; null on any failure (the order may\n * not be readable with the visitor's token — thank without order facts then).\n * GET /ecom/v1/orders/{id} → { order }\n */\nexport async function fetchDonationReceipt(orderId) {\n if (!orderId)\n return null;\n try {\n const res = await wixRequest(`/ecom/v1/orders/${orderId}`, { method: \"GET\" });\n return res?.order ? toReceipt(res.order) : null;\n }\n catch {\n return null;\n }\n}\n",
17 "js/wix/donation-store.js": "// Declarations of donation-store.ts: what this file exports, their arguments and what they return.\n// import { type DonatePaths } from \"./donate.js\";\n// import { type DonationField, type DonationSelection } from \"./donations-core.js\";\n// import type { CampaignDetail, DonationErrorCode, DonationFrequency } from \"./donations-types.js\";\n// export interface DonationStoreOptions {\n// origin?: string;\n// paths?: DonatePaths;\n// }\n// export interface DonationState extends DonationSelection {\n// amount: number | null;\n// fee: number | null;\n// feeLabel: string;\n// total: number | null;\n// totalLabel: string;\n// buttonLabel: string;\n// errors: Partial<Record<DonationField, DonationErrorCode>>;\n// messages: Partial<Record<DonationField, string>>;\n// showErrors: boolean;\n// valid: boolean;\n// submitting: boolean;\n// error: string | null;\n// available: boolean;\n// }\n// export interface DonationStore {\n// getState(): DonationState;\n// subscribe(listener: (state: DonationState) => void): () => void;\n// selectPreset(amount: number): void;\n// selectCustom(): void;\n// setCustomAmount(text: string): void;\n// setFrequency(frequency: DonationFrequency): void;\n// setCoverFee(coverFee: boolean): void;\n// setNote(note: string): void;\n// donate(): Promise<void>;\n// }\n// export declare function createDonationStore(campaign: CampaignDetail, { origin, paths }?: DonationStoreOptions): DonationStore;\n// The donate form for one campaign as a framework-free store — the logic behind useDonation,\n// usable from React (the hook wraps it), from a static page's campaign view, from Vue/Svelte, or as\n// the specification for a port. Frequency, preset or custom amount, the fee, the note, validation\n// with the widget's codes, the live button label, and donate() which navigates the FULL document\n// to the Wix-hosted checkout. All correctness (the line item, the checkout, the redirect session)\n// lives in the data layer; every rule (defaults, validation, fee, label) in donations-core.\n//\n// One store per campaign surface: createDonationStore(campaign), not a singleton. A static site\n// passes `paths` so the hosted checkout returns to its files (`thank-you.html`).\nimport { donationCheckoutUrl } from \"./donate.js\";\nimport { defaultSelection, donateLabel, errorMessage, feeFor, formatAmount, selectedAmount, toInput, totalFor, validateDonation, } from \"./donations-core.js\";\nexport function createDonationStore(campaign, { origin, paths } = {}) {\n const options = campaign.options;\n let sel = defaultSelection(options);\n let showErrors = false;\n let submitting = false;\n let error = null;\n const listeners = new Set();\n const derive = () => {\n const amount = selectedAmount(sel);\n const fee = options.askCoverFee && sel.coverFee ? feeFor(amount) : null;\n const total = totalFor(amount, sel.coverFee, options.askCoverFee);\n const errors = validateDonation(options, sel);\n const messages = {};\n for (const [field, code] of Object.entries(errors))\n messages[field] = errorMessage(code, options);\n return {\n ...sel,\n amount,\n fee,\n feeLabel: formatAmount(fee, options.currency),\n total,\n totalLabel: formatAmount(total, options.currency),\n buttonLabel: donateLabel(total, sel.frequency, options.currency),\n errors,\n messages,\n showErrors,\n valid: Object.keys(errors).length === 0,\n submitting,\n error,\n available: campaign.acceptsDonations,\n };\n };\n let state = derive();\n const emit = () => {\n state = derive();\n for (const fn of listeners)\n fn();\n };\n const update = (patch) => {\n if (submitting)\n return;\n sel = { ...sel, ...patch };\n emit();\n };\n return {\n getState: () => state,\n subscribe(fn) {\n const listener = () => { try {\n fn(state);\n }\n catch (e) {\n console.error(\"donation-store listener failed\", e);\n } };\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n selectPreset: (amount) => update({ presetAmount: amount, customMode: false, customAmount: \"\" }),\n selectCustom: () => update({ customMode: true, presetAmount: null }),\n setCustomAmount: (text) => update({ customAmount: text }),\n setFrequency: (frequency) => update({ frequency }),\n setCoverFee: (coverFee) => update({ coverFee }),\n setNote: (note) => update({ note: note.slice(0, options.commentMaxLength) }),\n async donate() {\n if (submitting)\n return; // a second click while the checkout is being created is ignored\n showErrors = true;\n error = null;\n if (!campaign.acceptsDonations) {\n error = \"This campaign isn't accepting donations right now.\";\n emit();\n throw new Error(error);\n }\n const input = toInput(options, sel);\n if (!input) {\n emit();\n return;\n }\n submitting = true;\n emit();\n try {\n window.location.href = await donationCheckoutUrl(campaign.id, input, { origin, paths });\n // stays \"submitting\" — the browser is leaving for the hosted checkout\n }\n catch (e) {\n submitting = false;\n error = \"We couldn't open checkout. Please try again.\";\n emit();\n throw e; // the caller retains the original API status/body; never replace the cause\n }\n },\n };\n}\n",
18 "js/wix/donations-core.js": "// Declarations of donations-core.ts: what this file exports, their arguments and what they return.\n// import type { AmountPreset, CampaignDetail, CampaignStatus, CampaignSummary, DonationErrorCode, DonationFrequency, DonationInput, DonationOptions, DonationReceipt, FrequencyOption, GoalProgress } from \"./donations-types.js\";\n// export type Raw = Record<string, any>;\n// export type ImgSrc = (value: any, width: number, height: number) => string;\n// export declare const DONATIONS_APP_ID = \"333b456e-dd48-4d6b-b32b-9fd48d74e163\";\n// export declare const ECOM_PLATFORM_APP_ID = \"1380b703-ce81-ff05-f115-39571d94dfcd\";\n// export declare const FEE_RATE = 0.029;\n// export declare const NOTE_MAX = 100;\n// export declare const FREQUENCIES: DonationFrequency[];\n// export declare const FREQUENCY_LABELS: Record<DonationFrequency, string>;\n// export declare const rawId: (raw: Raw | undefined | null) => string;\n// export interface CampaignsQueryOptions {\n// limit?: number;\n// }\n// export declare function campaignsQuery({ limit }?: CampaignsQueryOptions): Raw;\n// export declare function formatAmount(amount: number | null | undefined, currency: string | null | undefined): string;\n// export declare function formatMoney(money: Raw | null | undefined, currency: string | null | undefined): string;\n// export declare function statusOf(raw: Raw): CampaignStatus;\n// export declare const isArchived: (raw: Raw | null | undefined) => boolean;\n// export declare function campaignImage(raw: Raw): string;\n// export declare function currencyOf(metrics: Raw[] | null | undefined): string;\n// export declare function metricsFor(metrics: Raw[] | null | undefined, currency: string): {\n// donationCount: number;\n// totalAmount: Raw | undefined;\n// };\n// export declare function goalPercent(raised: number, target: number): number;\n// export declare function toGoal(campaignGoal: Raw | null | undefined, metrics: Raw[] | null | undefined, currency: string, now?: Date): GoalProgress | null;\n// export declare function presetsOf(raw: Raw, currency: string): AmountPreset[];\n// export declare function customAmountOf(raw: Raw, currency: string): DonationOptions[\"customAmount\"];\n// export declare function frequenciesOf(raw: Raw): FrequencyOption[];\n// export declare function optionsOf(raw: Raw, currency: string): DonationOptions;\n// export declare function toSummary(raw: Raw, metrics: Raw[] | null | undefined, currency: string, imgSrc: ImgSrc): CampaignSummary;\n// export declare function toDetail(raw: Raw, metrics: Raw[] | null | undefined, currency: string, imgSrc: ImgSrc): CampaignDetail;\n// export type DonationField = \"amount\" | \"customAmount\" | \"frequency\" | \"note\";\n// export interface DonationSelection {\n// frequency: DonationFrequency | null;\n// presetAmount: number | null;\n// customMode: boolean;\n// customAmount: string;\n// coverFee: boolean;\n// note: string;\n// }\n// export declare function defaultSelection(options: DonationOptions): DonationSelection;\n// export declare function parseAmount(text: string): number | null;\n// export declare function selectedAmount(sel: DonationSelection): number | null;\n// export declare function feeFor(amount: number | null): number | null;\n// export declare function totalFor(amount: number | null, coverFee: boolean, askCoverFee: boolean): number | null;\n// export declare function validateDonation(options: DonationOptions, sel: DonationSelection): Partial<Record<DonationField, DonationErrorCode>>;\n// export declare function errorMessage(code: DonationErrorCode, options: DonationOptions): string;\n// export declare function donateLabel(total: number | null, frequency: DonationFrequency | null, currency: string): string;\n// export declare function toInput(options: DonationOptions, sel: DonationSelection): DonationInput | null;\n// export declare function donationLineItem(campaignId: string, input: DonationInput): Raw;\n// export declare function cartBody(campaignId: string, input: DonationInput): Raw;\n// export declare function cartIdOf(res: Raw | null | undefined): string;\n// export interface DonatePaths {\n// thankYou?: string;\n// campaign?: string;\n// }\n// export declare function redirectBody(checkoutId: string, campaignId: string, origin: string, paths?: DonatePaths): Raw;\n// export declare function redirectUrl(session: Raw | null | undefined): string;\n// export declare function checkoutError(e: unknown, stage?: \"cart\" | \"redirect\", cartId?: string): Error & {\n// stage?: string;\n// cartId?: string;\n// };\n// export declare function orderIdFromSearch(search: string): string;\n// export declare function toReceipt(order: Raw): DonationReceipt;\n/** App id of Wix Donations — the `appId` of every donation line item's catalogReference. */\nexport const DONATIONS_APP_ID = \"333b456e-dd48-4d6b-b32b-9fd48d74e163\";\n/** App id of the eCom platform (Checkout & Orders) — a donation is an eCom checkout. */\nexport const ECOM_PLATFORM_APP_ID = \"1380b703-ce81-ff05-f115-39571d94dfcd\";\n/** Wix's fixed processing-fee rate a donor may add (askDonorCoverFee); applied to every recurring charge. */\nexport const FEE_RATE = 0.029;\n/** The donor note's maximum length (the Wix widget's default). */\nexport const NOTE_MAX = 100;\nexport const FREQUENCIES = [\"ONE_TIME\", \"WEEK\", \"MONTH\", \"YEAR\"];\nexport const FREQUENCY_LABELS = {\n ONE_TIME: \"One-time\",\n WEEK: \"Weekly\",\n MONTH: \"Monthly\",\n YEAR: \"Yearly\",\n};\nexport const rawId = (raw) => raw?._id ?? raw?.id ?? \"\";\n/**\n * The Query Donation Campaigns body both transports send: only non-archived campaigns (archived =\n * hidden and cannot accept donations), oldest first (the Wix widget's default campaign is the first\n * by creation date), cursor paging. `status` is NOT filterable — closed campaigns are filtered by the\n * caller from the DTO. The SDK builder spells the same as `.eq(\"archived\", false).ascending(\"_createdDate\")`.\n */\nexport function campaignsQuery({ limit = 100 } = {}) {\n if (!Number.isInteger(limit) || limit < 1 || limit > 100)\n throw new Error(\"limit must be between 1 and 100.\");\n return { filter: { archived: false }, sort: [{ fieldName: \"createdDate\", order: \"ASC\" }], cursorPaging: { limit } };\n}\n// ---- money ----------------------------------------------------------------------------------------\n/**\n * A number in a currency, in the visitor's locale; whole amounts without decimals (\"$25\", \"$12.50\").\n * \"\" when the currency is unknown — never a bare number posing as money, never an assumed USD.\n */\nexport function formatAmount(amount, currency) {\n if (amount == null || !Number.isFinite(amount) || !currency)\n return \"\";\n const whole = Number.isInteger(amount);\n try {\n return new Intl.NumberFormat(undefined, {\n style: \"currency\",\n currency,\n minimumFractionDigits: whole ? 0 : 2,\n maximumFractionDigits: 2,\n }).format(amount);\n }\n catch {\n return `${amount} ${currency}`;\n }\n}\n/**\n * MultiCurrencyPrice { amount, convertedAmount, formattedAmount } → a display string. The API's\n * own `formattedAmount` wins (the site's currency symbol and locale); otherwise the amount is\n * formatted in `currency`; \"\" when neither is available.\n */\nexport function formatMoney(money, currency) {\n if (money?.formattedConvertedAmount)\n return String(money.formattedConvertedAmount);\n if (money?.formattedAmount)\n return String(money.formattedAmount);\n const value = money?.convertedAmount ?? money?.amount;\n if (value == null || value === \"\")\n return \"\";\n return formatAmount(Number(value), currency);\n}\nconst amountOf = (money) => {\n const n = Number(money?.amount ?? 0);\n return Number.isFinite(n) ? n : 0;\n};\n// ---- campaign rules -------------------------------------------------------------------------------\n/** `status` is read-only and computed by Wix; anything unknown reads as collecting. */\nexport function statusOf(raw) {\n if (raw.status === \"GOAL_REACHED\")\n return \"goalReached\";\n if (raw.status === \"EXPIRED\")\n return \"expired\";\n return \"collecting\";\n}\nexport const isArchived = (raw) => raw?.archived === true;\n/**\n * The cover image as a value imgSrc resolves: the SDK hands a `wix:image://…` string; REST hands\n * an Image object { id, url } — its url when present, else the media id in the wix:image form the\n * URL builder scales. \"\" when the campaign has no image.\n */\nexport function campaignImage(raw) {\n const img = raw.coverImage;\n if (!img)\n return \"\";\n if (typeof img === \"string\")\n return img;\n if (img.url)\n return String(img.url);\n return img.id ? `wix:image://v1/${img.id}/${img.id}` : \"\";\n}\n/**\n * The currency the campaign's amounts are in. The metrics endpoint \"currently returns only the\n * site's default currency\", so its first entry's currencyCode is the site currency. \"\" when the\n * metrics could not be read (the API's formattedAmount strings still display; only the live\n * custom-amount total loses its label).\n */\nexport function currencyOf(metrics) {\n return metrics?.find((m) => m?.currencyCode)?.currencyCode ?? \"\";\n}\n/** The metrics entry for `currency` (the first entry when no currency is known); zero when absent. */\nexport function metricsFor(metrics, currency) {\n const list = metrics ?? [];\n const entry = currency ? list.find((m) => m?.currencyCode === currency) : list[0];\n return { donationCount: Number(entry?.donationCount ?? 0) || 0, totalAmount: entry?.totalAmount };\n}\n/** Wix's progress rounding: 0 target → 0; under 1% rounds up, over 99% rounds down, else nearest. */\nexport function goalPercent(raised, target) {\n if (!target)\n return 0;\n const v = (raised / target) * 100;\n if (v < 1)\n return Math.ceil(v);\n if (v > 99)\n return Math.floor(v);\n return Math.round(v);\n}\nconst sameDay = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n/** campaignGoal + metrics → GoalProgress; null when the campaign has no goal. `now` is injectable for tests. */\nexport function toGoal(campaignGoal, metrics, currency, now = new Date()) {\n if (!campaignGoal?.targetAmount)\n return null;\n const m = metricsFor(metrics, currency);\n const raisedAmount = amountOf(m.totalAmount);\n const targetAmount = amountOf(campaignGoal.targetAmount);\n const end = campaignGoal.endDate ? new Date(campaignGoal.endDate) : null;\n const hasEnd = !!end && !Number.isNaN(end.getTime());\n const ended = hasEnd && end.getTime() < now.getTime();\n const left = hasEnd && !ended ? end.getTime() - now.getTime() : 0;\n return {\n raised: metrics == null ? \"\" : m.totalAmount ? formatMoney(m.totalAmount, currency) : formatAmount(0, currency),\n target: formatMoney(campaignGoal.targetAmount, currency),\n raisedAmount,\n targetAmount,\n percent: goalPercent(raisedAmount, targetAmount),\n donationCount: m.donationCount,\n reached: raisedAmount >= targetAmount,\n endDate: hasEnd ? end.toISOString() : null,\n ended,\n lastDay: hasEnd && sameDay(end, now),\n daysLeft: left ? Math.floor(left / 86_400_000) + 1 : 0,\n hoursLeft: left ? Math.floor(left / 3_600_000) + 1 : 0,\n };\n}\nexport function presetsOf(raw, currency) {\n return (raw.predefinedDonationAmounts ?? [])\n .map((p) => ({ amount: amountOf(p.price), label: formatMoney(p.price, currency), impact: p.description ?? \"\" }))\n .filter((p) => p.amount > 0);\n}\n/** customAmountEnabled + customAmountOptions; a 0 or absent limit means no limit. */\nexport function customAmountOf(raw, currency) {\n const o = raw.customAmountOptions ?? {};\n const min = amountOf(o.minimum) || null;\n const max = amountOf(o.maximum) || null;\n return {\n enabled: raw.customAmountEnabled === true,\n min,\n max,\n minLabel: min ? formatMoney(o.minimum, currency) : \"\",\n maxLabel: max ? formatMoney(o.maximum, currency) : \"\",\n };\n}\n/** donationFrequencies in API order, labelled; unknown values dropped; ONE_TIME when the list is empty. */\nexport function frequenciesOf(raw) {\n const list = (raw.donationFrequencies ?? []).filter((f) => FREQUENCIES.includes(f));\n return (list.length ? list : [\"ONE_TIME\"]).map((value) => ({ value, label: FREQUENCY_LABELS[value] }));\n}\nexport function optionsOf(raw, currency) {\n return {\n currency,\n presets: presetsOf(raw, currency),\n customAmount: customAmountOf(raw, currency),\n frequencies: frequenciesOf(raw),\n askCoverFee: raw.askDonorCoverFee === true,\n feeRate: FEE_RATE,\n commentsEnabled: raw.commentsEnabled === true,\n commentMaxLength: NOTE_MAX,\n };\n}\n// ---- DTO mappers -----------------------------------------------------------------------------------\nexport function toSummary(raw, metrics, currency, imgSrc) {\n const status = statusOf(raw);\n return {\n id: rawId(raw),\n name: raw.name ?? \"\",\n status,\n acceptsDonations: status === \"collecting\",\n imageUrl: imgSrc(campaignImage(raw), 1200, 675),\n goal: toGoal(raw.campaignGoal, metrics, currency),\n };\n}\nexport function toDetail(raw, metrics, currency, imgSrc) {\n return { ...toSummary(raw, metrics, currency, imgSrc), options: optionsOf(raw, currency) };\n}\n/**\n * The widget's defaults: the first preset, the first frequency, cover-fee pre-checked when the\n * owner asks for it, and custom mode forced when the campaign offers no presets.\n */\nexport function defaultSelection(options) {\n const customOnly = options.customAmount.enabled && options.presets.length === 0;\n return {\n frequency: options.frequencies[0]?.value ?? null,\n presetAmount: customOnly ? null : options.presets[0]?.amount ?? null,\n customMode: customOnly,\n customAmount: \"\",\n coverFee: options.askCoverFee,\n note: \"\",\n };\n}\n/** \"1,250.50\" / \" 25 \" → 1250.5; null when not a number. */\nexport function parseAmount(text) {\n const cleaned = text.replace(/[\\s,]/g, \"\");\n if (!cleaned)\n return null;\n const n = Number(cleaned);\n return Number.isFinite(n) ? n : null;\n}\nconst decimalsOf = (n) => {\n const s = String(n);\n const dot = s.indexOf(\".\");\n return dot === -1 ? 0 : s.length - dot - 1;\n};\n/** The amount the selection resolves to (custom text parsed in custom mode); null when there is none. */\nexport function selectedAmount(sel) {\n return sel.customMode ? parseAmount(sel.customAmount) : sel.presetAmount;\n}\n/** The donor's fee at Wix's fixed 2.9%, rounded to cents; null for a non-positive amount. */\nexport function feeFor(amount) {\n if (amount == null || amount <= 0)\n return null;\n return Math.round(amount * FEE_RATE * 100) / 100;\n}\n/** amount + fee when the owner asks and the donor agreed; the amount alone otherwise. */\nexport function totalFor(amount, coverFee, askCoverFee) {\n if (amount == null)\n return null;\n const fee = askCoverFee && coverFee ? feeFor(amount) ?? 0 : 0;\n return Math.round((amount + fee) * 100) / 100;\n}\n/** The widget's validation, field by field; {} when the selection is valid. */\nexport function validateDonation(options, sel) {\n const errors = {};\n if (sel.customMode) {\n const n = parseAmount(sel.customAmount);\n const { min, max } = options.customAmount;\n if (n == null || n <= 0)\n errors.customAmount = \"MISSING_AMOUNT\";\n else if (decimalsOf(n) > 2)\n errors.customAmount = \"TOO_MANY_DECIMALS\";\n else if (min != null && n < min)\n errors.customAmount = \"BELOW_MIN_AMOUNT\";\n else if (max != null && n > max)\n errors.customAmount = \"ABOVE_MAX_AMOUNT\";\n }\n else if (sel.presetAmount == null || sel.presetAmount <= 0) {\n errors.amount = \"MISSING_AMOUNT\";\n }\n if (!sel.frequency)\n errors.frequency = \"MISSING_FREQUENCY\";\n if (sel.note.length > options.commentMaxLength)\n errors.note = \"NOTE_TOO_LONG\";\n return errors;\n}\n/** Visitor-facing text for a validation code, with the campaign's own limits. */\nexport function errorMessage(code, options) {\n switch (code) {\n case \"MISSING_AMOUNT\":\n return \"Enter an amount.\";\n case \"TOO_MANY_DECIMALS\":\n return \"Use at most two decimal places.\";\n case \"BELOW_MIN_AMOUNT\":\n return `The minimum donation is ${options.customAmount.minLabel || options.customAmount.min}.`;\n case \"ABOVE_MAX_AMOUNT\":\n return `The maximum donation is ${options.customAmount.maxLabel || options.customAmount.max}.`;\n case \"MISSING_FREQUENCY\":\n return \"Choose how often to donate.\";\n case \"NOTE_TOO_LONG\":\n return `Keep your note under ${options.commentMaxLength} characters.`;\n }\n}\n/** \"Donate $25\" / \"Donate $25.73 Monthly\" / \"Donate\" when there is no amount or no known currency. */\nexport function donateLabel(total, frequency, currency) {\n const amount = formatAmount(total, currency);\n const cadence = frequency && frequency !== \"ONE_TIME\" ? ` ${FREQUENCY_LABELS[frequency]}` : \"\";\n return amount ? `Donate ${amount}${cadence}` : \"Donate\";\n}\n/** A valid selection → the checkout input; null when validation fails. */\nexport function toInput(options, sel) {\n if (Object.keys(validateDonation(options, sel)).length)\n return null;\n const amount = selectedAmount(sel);\n if (amount == null || !sel.frequency)\n return null;\n return { amount, frequency: sel.frequency, coverFee: options.askCoverFee && sel.coverFee, note: sel.note.trim() };\n}\n// ---- cart -----------------------------------------------------------------------------------------\n/**\n * The one catalog item a donation cart carries. `amount` is a NUMBER and `frequency` the enum\n * string; `donorCoveringFees` is sent only when the donor opted in (the Donations catalog plugin\n * prices the line from these options — never a customLineItem, never a computed price).\n */\nexport function donationLineItem(campaignId, input) {\n if (!campaignId)\n throw new Error(\"A campaign id is required to donate.\");\n if ((typeof input.amount !== \"number\" && typeof input.amount !== \"string\") || String(input.amount).trim() === \"\")\n throw new Error(\"Enter a positive donation amount.\");\n const amount = Number(input.amount);\n if (!Number.isFinite(amount) || amount <= 0)\n throw new Error(\"Enter a positive donation amount.\");\n if (!FREQUENCIES.includes(input.frequency))\n throw new Error(\"Choose a supported donation frequency.\");\n return {\n quantity: 1,\n catalogReference: {\n appId: DONATIONS_APP_ID,\n catalogItemId: campaignId,\n options: { amount, ...(input.frequency !== \"ONE_TIME\" ? { frequency: input.frequency } : {}), ...(input.coverFee ? { donorCoveringFees: true } : {}) },\n },\n };\n}\n/**\n * Create Cart body (Cart V2): the single donation catalog item, the WEB channel on the cart's\n * `source`, the donor note as the cart's `note`. In V2 the created cart IS the checkout — its id is\n * the id the redirect session takes; there is no separate Create Checkout step.\n */\nexport function cartBody(campaignId, input) {\n return {\n cart: { source: { channelType: \"WEB\" }, ...(input.note ? { note: input.note } : {}) },\n catalogItems: [donationLineItem(campaignId, input)],\n };\n}\n/**\n * The cart id out of the Create Cart response — the id a V2 donation hands the redirect session\n * (the cart is the checkout). The SDK returns a Cart directly (`_id`); REST wraps it as\n * `{ cart: { id } }`.\n */\nexport function cartIdOf(res) {\n const id = res?._id ?? res?.id ?? res?.cart?._id ?? res?.cart?.id ?? \"\";\n if (!id)\n throw new Error(\"Checkout couldn't start: no cart id returned.\");\n return String(id);\n}\n/**\n * The Create Redirect Session body for a donation checkout. `origin` is the published https host\n * (window.location.origin in a browser) — an http or server-derived origin isn't on the OAuth app's\n * allowed domains and 403s the return; no callbacks when unknown (SSR).\n */\nexport function redirectBody(checkoutId, campaignId, origin, paths = {}) {\n if (!checkoutId)\n throw new Error(\"A checkout id is required to start the hosted checkout.\");\n const callbacks = origin\n ? {\n postFlowUrl: `${origin}${paths.campaign ?? `/donate/${campaignId}`}`,\n thankYouPageUrl: `${origin}${paths.thankYou ?? \"/donate/thank-you\"}`,\n }\n : {};\n return { ecomCheckout: { checkoutId }, callbacks };\n}\n/** The hosted checkout URL out of a redirect-session response; throws when there is none. */\nexport function redirectUrl(session) {\n const url = session?.redirectSession?.fullUrl;\n if (!url)\n throw new Error(\"Checkout couldn't start — please try again.\");\n return url;\n}\n/** Preserve diagnostic status/body/cause; payment setup must not be guessed from a generic failure. */\nexport function checkoutError(e, stage, cartId) {\n const error = e instanceof Error ? e : new Error(String(e) || \"Checkout couldn't start — please try again.\", { cause: e });\n if (stage)\n error.stage = stage;\n if (cartId)\n error.cartId = cartId;\n return error;\n}\n// ---- the receipt ----------------------------------------------------------------------------------\n/**\n * The order id the hosted checkout appends to thankYouPageUrl: the redirect-session contract\n * spells it `orderId`; the Wix widget's own success URL uses `orderid`. Both are read.\n */\nexport function orderIdFromSearch(search) {\n const q = new URLSearchParams(search);\n return q.get(\"orderId\") ?? q.get(\"orderid\") ?? \"\";\n}\n/**\n * An eCom order → the receipt: the donor's name from billingInfo.contactDetails (recipientInfo only\n * when billing lacks a complete name), the order total's formatted amount, paid only on PAID.\n */\nexport function toReceipt(order) {\n const complete = (c) => !!(c?.firstName && c?.lastName);\n const billing = order.billingInfo?.contactDetails;\n const recipient = order.recipientInfo?.contactDetails;\n const contact = complete(billing) ? billing : complete(recipient) ? recipient : billing ?? recipient ?? {};\n const total = order.priceSummary?.total;\n return {\n orderNumber: order.number != null ? String(order.number) : \"\",\n donorFirstName: contact.firstName ?? \"\",\n donorLastName: contact.lastName ?? \"\",\n amount: formatMoney(total, order.currency ?? \"\"),\n paid: order.paymentStatus === \"PAID\",\n campaignId: order.lineItems?.[0]?.catalogReference?.catalogItemId ?? \"\",\n };\n}\n",
19 "js/wix/donations-types.js": "// Declarations of donations-types.ts: what this file exports, their arguments and what they return.\n// /** The Donation Campaigns `Frequency` enum values. */\n// export type DonationFrequency = \"ONE_TIME\" | \"WEEK\" | \"MONTH\" | \"YEAR\";\n// /** `status` as the API computes it: COLLECTING → collecting, GOAL_REACHED → goalReached, EXPIRED → expired. */\n// export type CampaignStatus = \"collecting\" | \"goalReached\" | \"expired\";\n// /** Goal progress from the campaign's goal + the metrics read (null on the DTO when the campaign has no goal). */\n// export interface GoalProgress {\n// /** Formatted total raised (\"$1,250\"); \"\" when the metrics could not be read. */\n// raised: string;\n// /** Formatted target (\"$20,000\"). */\n// target: string;\n// /** Numbers for a11y and bar width only — never render them as money. */\n// raisedAmount: number;\n// targetAmount: number;\n// /** Wix's rounding (<1 → ceil, >99 → floor, else round); may exceed 100. */\n// percent: number;\n// donationCount: number;\n// /** raisedAmount >= targetAmount (independent of `status`: a campaign may keep collecting past its goal). */\n// reached: boolean;\n// /** ISO end date, null when the goal has no deadline. */\n// endDate: string | null;\n// /** endDate is in the past. */\n// ended: boolean;\n// /** endDate falls on today's calendar day. */\n// lastDay: boolean;\n// /** Whole days left including today; 0 with no deadline or once ended. */\n// daysLeft: number;\n// /** Whole hours left including the current one; 0 with no deadline or once ended. */\n// hoursLeft: number;\n// }\n// export interface AmountPreset {\n// amount: number;\n// /** Formatted (\"$25\"). */\n// label: string;\n// /** The owner's impact statement for this amount; \"\" when none. */\n// impact: string;\n// }\n// export interface FrequencyOption {\n// value: DonationFrequency;\n// /** \"One-time\" | \"Weekly\" | \"Monthly\" | \"Yearly\". */\n// label: string;\n// }\n// export interface DonationOptions {\n// /** ISO currency code the campaign's amounts are in; \"\" when it could not be determined. */\n// currency: string;\n// /** In API order; the first is the default selection. [] with a custom-only campaign. */\n// presets: AmountPreset[];\n// customAmount: {\n// enabled: boolean;\n// /** null = no limit. */\n// min: number | null;\n// max: number | null;\n// /** Formatted limits for messages; \"\" when none. */\n// minLabel: string;\n// maxLabel: string;\n// };\n// /** >= 1; the first is the default. Show a picker only when length > 1. */\n// frequencies: FrequencyOption[];\n// /** The owner asks donors to add the processing fee. */\n// askCoverFee: boolean;\n// /** 0.029 — Wix's fixed rate, applied to every recurring charge too. */\n// feeRate: number;\n// /** The owner enabled the donor note. */\n// commentsEnabled: boolean;\n// /** Max note length (100). */\n// commentMaxLength: number;\n// }\n// /** A campaign as a listing card or a home strip needs it. */\n// export interface CampaignSummary {\n// id: string;\n// name: string;\n// status: CampaignStatus;\n// /** status === \"collecting\". An archived campaign never reaches a DTO. */\n// acceptsDonations: boolean;\n// /** Resolved https URL (\"\" when the campaign has no cover image). */\n// imageUrl: string;\n// /** null when the campaign has no goal. */\n// goal: GoalProgress | null;\n// }\n// /** A campaign as the campaign page and the donate form need it. */\n// export interface CampaignDetail extends CampaignSummary {\n// options: DonationOptions;\n// }\n// /** What the visitor chose — what the checkout line carries. */\n// export interface DonationInput {\n// amount: number;\n// frequency: DonationFrequency;\n// coverFee: boolean;\n// note: string;\n// }\n// export type DonationErrorCode = \"MISSING_AMOUNT\" | \"TOO_MANY_DECIMALS\" | \"BELOW_MIN_AMOUNT\" | \"ABOVE_MAX_AMOUNT\" | \"MISSING_FREQUENCY\" | \"NOTE_TOO_LONG\";\n// /** The eCom order behind a completed donation, as the thank-you page shows it. */\n// export interface DonationReceipt {\n// orderNumber: string;\n// donorFirstName: string;\n// donorLastName: string;\n// /** Formatted order total; \"\" when the order carries no formatted amount and no currency. */\n// amount: string;\n// /** paymentStatus === \"PAID\". Anything else reads as pending — never \"paid\". */\n// paid: boolean;\n// campaignId: string;\n// }\n// Donations DTOs — the serializable shapes every hook, component, and page consumes. Plain JSON:\n// safe as Astro island props or across server/client boundaries. Images are resolved https URLs;\n// every displayable amount is a ready formatted string (\"\" when the currency is unknown).\nexport {};\n",
20 "js/wix/receipt-store.js": "// Declarations of receipt-store.ts: what this file exports, their arguments and what they return.\n// import type { DonationReceipt } from \"./donations-types.js\";\n// export interface ReceiptState {\n// orderId: string;\n// loading: boolean;\n// receipt: DonationReceipt | null;\n// }\n// export interface ReceiptStore {\n// getState(): ReceiptState;\n// subscribe(listener: (state: ReceiptState) => void): () => void;\n// start(): void;\n// stop(): void;\n// }\n// export declare function createReceiptStore(orderId?: string): ReceiptStore;\n// The thank-you page's order read as a framework-free store — the logic behind useDonationReceipt.\n// Landing on the thank-you URL is Wix's success redirect (it is reached only after the hosted\n// checkout completes); the order itself may or may not be readable with the visitor's token, so\n// `receipt` null with `loading` false is a normal outcome, not an error — the page thanks the donor\n// without order facts. One store per page: createReceiptStore(orderId).\nimport { fetchDonationReceipt } from \"./donate.js\";\nimport { orderIdFromSearch } from \"./donations-core.js\";\n/** `orderId` omitted → read from the current URL (`?orderId=` or `?orderid=`). */\nexport function createReceiptStore(orderId) {\n const id = orderId ?? (typeof window !== \"undefined\" ? orderIdFromSearch(window.location.search) : \"\");\n let state = { orderId: id, loading: !!id, receipt: null };\n let started = false;\n const listeners = new Set();\n function setState(patch) {\n state = { ...state, ...patch };\n for (const fn of listeners)\n fn();\n }\n return {\n getState: () => state,\n subscribe(fn) {\n const listener = () => { try {\n fn(state);\n }\n catch (e) {\n console.error(\"receipt-store listener failed\", e);\n } };\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n start() {\n if (started)\n return;\n started = true;\n if (!id)\n return;\n fetchDonationReceipt(id).then((receipt) => { if (started)\n setState({ receipt, loading: false }); });\n },\n stop() {\n started = false;\n },\n };\n}\n"