Skill 16 · Wix Headless Templates
Subchapter 16.228
donations/INSTRUCTIONS.mdMarkdown26 KBView on GitHub
The donations machinery ships as files — campaign reads (list + by id, with goal progress), the exact donation → hosted-checkout sequence, the donate form, the goal progress block, the thank-you island, typed end-to-end. The presentation is yours: you design and implement the campaign card and the campaigns index, the campaign page’s layout around the shipped form, the home page and the brand. You never write checkout logic; you never skip designing.
Read Donations experience (opens in a new tab) before designing the donor surfaces. If that sibling skill is not installed, read the published overlay (opens in a new tab). It supplies the short, framework-independent page/form quality bar; the code contracts below supply the implementation. Apply its intent/draft recovery requirements in the UI layer: extend the supplied form/store where needed, retaining its validation and checkout exports. In-memory defaults alone do not survive an external checkout. Do not put donor notes in route parameters or reset them on an unverified return.
A donation is an eCom cart (Cart V2) with one line that references the campaign; in V2 the cart IS the
checkout, so the cart’s id is the id the redirect session takes — there is no separate checkout entity.
The cart’s donation options.amount is a number, even though campaign price amounts are strings.
Omit options.frequency for one-time giving; send only WEEK, MONTH, YEAR for recurring giving.
Wix’s hosted checkout takes the payment and the donor’s details, and the eCom order is the receipt. The campaign entity carries
no description and no slug: the story on a campaign page is the site’s own copy, and routes key by
the campaign id (/donate/<id>).
On Astro and React the shipped files are tested and work as they are — this table and the
contracts below are everything you need to use them, so don’t spend the run reading their source;
wire them and build your surfaces. Reading them is the right move when something is off (a runtime
error, a field this playbook doesn’t cover) or when the brief wants a behaviour they don’t offer —
then read the file that owns it and change or extend it. On lib, static, and a port the
components don’t deploy at all, and each wiring section below opens with the files to read before
writing their equivalents. Files you edit: SiteLayout.astro, styles/global.css, and the story
copy on the campaign page. Files you create (skeletons below): your campaigns index island (or
Astro markup), your campaign page layout, and your home page.
| file | what it is |
|---|---|
wix/config.ts · wix/sdk.ts | shared auth seam (deploy configures it — nothing to set by hand) |
wix/media.ts · wix/money.ts | imgAttrs(url, sizes, ratio) — every <img> attribute for a DTO image: <img {...imgAttrs(c.imageUrl, "50vw", 9 / 16)} alt={c.name} />; imgSrc() / imgSrcSet() underneath |
wix/donations/types.ts | the DTOs (CampaignSummary, CampaignDetail, GoalProgress, DonationOptions, DonationReceipt) — contracts inlined below |
wix/donations/campaigns.ts | fetchCampaigns, fetchCampaign(id), fetchDefaultCampaign — the transport; the rules and DTO mappers are in donations-core.ts beside it (shared with the REST layer) |
wix/donations/donate.ts | donationCheckoutUrl(campaignId, input) — the donation cart + hosted redirect session; fetchDonationReceipt(orderId) — the thank-you page’s order read; bodies in donations-core.ts |
wix/donations/donation-store.ts · campaigns-store.ts · receipt-store.ts | the form, listing and receipt state machines, framework-free (createDonationStore(campaign), createCampaignsStore(), createReceiptStore() — getState/subscribe + actions, one instance per surface); the hooks below bind them to React, every other stack uses them directly |
hooks/donations/useDonation.ts | React binding of donation-store.ts: the donate form’s state and actions — contract below |
hooks/donations/useCampaigns.ts | React binding of campaigns-store.ts: the listing — contract below |
hooks/donations/useDonationReceipt.ts | React binding of receipt-store.ts: reads ?orderId= and the order — contract below |
components/donations/DonateForm.tsx | the donate form for one campaign — frequency picker only when > 1 frequency, presets with impact text, “Other amount” only when enabled (min/max messages), the cover-fee checkbox with the computed 2.9% only when asked, the note only when enabled, the live “Donate $25 Monthly” label, “Redirecting…” in flight, errors after the first attempt, the closed state as children — wire as-is (<DonateForm client:load campaign={campaign}>closing message</DonateForm>) |
components/donations/GoalProgress.tsx | raised of target, the bar at Wix’s rounded percent, donation count, time left (“3 days left” / hours on the last day / “Ended”) — renders nothing without a goal; no hooks, so it renders on the server — wire as-is on cards, the campaign page, a home strip (compact for cards) |
components/donations/DonationReceipt.tsx | the hosted checkout’s thank-you island — reads ?orderId=, shows the order’s facts only when the order could be read, an honest thank-you otherwise, a neutral page on a direct visit — wire as-is (client:only="react") |
styles/global.css | the design system: Tailwind v4 + the @theme token block (colors, radii, fonts — shared across verticals). Everything, shipped and yours, styles from these tokens |
Astro stack additionally gets:
| file | what it is |
|---|---|
layouts/SiteLayout.astro | site chrome — yours to brand (keep the seo-tags slot + global.css import; it takes image for og:image). If another vertical is also deployed, its layout won — add a Donate nav link there |
components/donations/CampaignPage.astro | REFERENCE campaign surface: cover, name, story prop, GoalProgress, DonateForm with the closed-state message — redesign it (it is mounted by both pages below); pass the campaign’s story copy through story |
pages/donate/index.astro | SSR campaigns index — keep the frontmatter (one campaign → renders CampaignPage directly, the Wix widget’s default-campaign rule); the card markup is yours |
pages/donate/[id].astro | SSR campaign page, a real 404 on a missing or archived id — keep the frontmatter (plain <title>/<meta>/og:image from the DTO: Donations has no owner-editable SEO item type, so campaign pages don’t appear in the Wix SEO panel or sitemap) |
pages/donate/thank-you.astro | the hosted checkout’s return (?orderId=) — keep the route; the shipped redirect callbacks point at it |
GoalProgress compact,
a Donate link; a closed campaign reads “Goal reached” / “Ended”, never “Donate”) and grid rhythm, an
honest empty state. The index page ships with SSR data; render it in Astro markup or swap in an
island on useCampaigns.GoalProgress, and the shipped DonateForm as the giving column; design it for the brand (a
sticky form column, an editorial split, a full-bleed cover). Donation logic never leaves the shipped
component and hook.fetchCampaigns({ limit: 1 }) in frontmatter →
name, GoalProgress, CTA to /donate/<id>), brand story.Plus the theme (@theme block, one edit) and the chrome (SiteLayout, one pass).
The shared Donations experience overlay above owns page composition, mobile density, truthful progress/copy and return-state quality. The contracts below own the wiring; apply both rather than maintaining a second set of design rules here.
// CampaignSummary (cards, strips) — display-ready:
// { id, name, status /* "collecting" | "goalReached" | "expired" */, acceptsDonations,
// imageUrl /* "" when none */,
// goal: GoalProgress | null /* null = no goal */ }
// GoalProgress: { raised /* "$1,250" | "" when unreadable */, target /* "$20,000" */,
// raisedAmount, targetAmount /* numbers for bar math only */, percent /* may exceed 100 */,
// donationCount, reached, endDate /* ISO | null */, ended, lastDay, daysLeft, hoursLeft }
// CampaignDetail adds options: { currency /* "" when unknown */, presets: [{ amount, label, impact }],
// customAmount: { enabled, min, max, minLabel, maxLabel }, frequencies: [{ value, label }],
// askCoverFee, feeRate /* 0.029 */, commentsEnabled, commentMaxLength /* 100 */ }
// useCampaigns({ initialCampaigns? }) →
// { campaigns: CampaignSummary[]|null /* null = loading → skeletons; [] after a failed load, with error */, error }
// useDonation(campaign, { origin?, paths? }?) →
// { frequency, presetAmount, customMode, customAmount /* text */, coverFee, note,
// amount, fee, feeLabel, total, totalLabel, buttonLabel /* "Donate $25 Monthly" */,
// errors, messages /* per field, visitor-facing */, showErrors /* after the first attempt */, valid,
// submitting, error, available /* campaign.acceptsDonations */,
// selectPreset(n), selectCustom(), setCustomAmount(text), setFrequency(f), setCoverFee(b), setNote(s),
// donate(): Promise<void> } // resolves as the browser navigates to the hosted checkout (or with
// // messages showing); rejects with the original diagnostic error;
// // render state.error for the visitor, not the raw API response
// <DonateForm campaign={c} options? children? className? buttonClassName? /> wraps it: you only place
// it; it decides what to render, what to say, and shows its own errors.
// useDonationReceipt(orderId?) → { orderId /* "" on a direct visit */, loading, receipt: DonationReceipt|null }
// DonationReceipt: { orderNumber, donorFirstName, donorLastName, amount /* formatted */, paid, campaignId }The pages ship; the surfaces they render are yours. Hooks first, branches after (an early return
above a hook changes hook order between renders and React throws). client:load islands render on
the server too — render every state totally; nothing in a render path may throw.
// src/components/donations/CampaignsGrid.tsx — YOU build it when the index wants client state;
// pages/donate/index.astro can mount it with the SSR campaigns instead of its Astro card markup.
import { useCampaigns } from "../../hooks/donations/useCampaigns";
import GoalProgress from "./GoalProgress";
import type { CampaignSummary } from "../../wix/donations/types";
export default function CampaignsGrid(props: { initialCampaigns?: CampaignSummary[] /* SSR prop — pass straight to useCampaigns */ }) {
const { campaigns, error } = useCampaigns(props);
// …you implement the render:
// • error → a short inline message
// • campaigns === null → skeleton cards; [] → your honest empty state
// • else YOUR grid of YOUR cards: cover via imgAttrs(c.imageUrl, "33vw", 9 / 16) only when
// non-empty, the name (wraps — `min-w-0 break-words`), <GoalProgress goal={c.goal} compact />,
// a link to `/donate/${c.id}` reading "Donate" only when c.acceptsDonations ("Goal reached" /
// "Ended" otherwise) as the card's LAST ROW (`mt-auto`); never a button inside the card's <a>.
}// src/components/donations/CampaignPage.astro — ships as a reference; REDESIGN it in place (both
// pages mount it). Keep: <GoalProgress goal={campaign.goal} /> and
// <DonateForm client:load campaign={campaign}>{closing message}</DonateForm>. Yours: the cover
// (a bounded band on phones — `max-h-[40vh]`, not a full-screen hero), the name, the story prop
// (your copy), the arrangement (the form in the first screen at 390px).On lib, static, and a port, nothing under components/ or hooks/ arrives (on static, read
static.md in this folder). The state machines
behind the hooks do arrive — wix/donations/donation-store.ts, campaigns-store.ts,
receipt-store.ts — so you never rewrite them: create a store per surface, subscribe, render from
getState(), call its actions. Their *State interfaces are the render contract; read those. What
you write is the rendering — grid, card, campaign page, the form, the thank-you page — and for that
read first:
components/donations/DonateForm.tsx — the form as working code: which controls appear for which
options, the preset/custom switch, the fee line, the note counter, when messages show, the
in-flight label, the inline error, the closed state.components/donations/GoalProgress.tsx — the progress block’s text rules (timeLeftLabel).components/donations/DonationReceipt.tsx — the thank-you page’s three honest states.Under templates/donations/app/.
@theme tokens (one edit); brand SiteLayout.astro (one pass — merge into the other
vertical’s layout instead if both are deployed).components/donations/CampaignPage.astro around the two shipped components and pass the
campaign’s story through story; restyle the card markup in pages/donate/index.astro (or mount
your CampaignsGrid there with initialCampaigns). Author your surfaces in as few messages as
possible — batch multiple Writes per message.pages/index.astro (home) — it exists from the scaffold; Read it before overwriting.Read the reference files listed above before writing any surface.
deploy.mjs donations --stack lib put the data layer in src/wix/ and nothing else: sdk.ts (the
visitor client, configured with the public client id), media.ts, money.ts, and wix/donations/ —
campaigns.ts, donate.ts, types.ts, donations-core.ts, and the three stores. None of it is
React. The hooks and components don’t ship on this stack; the stores replace the hooks, and you write
the components in your framework to the contracts on this page:
shallowRef updated in
subscribe; Svelte: readable(store.getState(), (set) => store.subscribe(() => set(store.getState())));
Solid: a signal set in subscribe). createCampaignsStore({ initialCampaigns? }) per listing
(start() when mounted, stop() when unmounted), createDonationStore(campaign, { paths? }) per
campaign surface (donate() navigates the document itself), createReceiptStore() on the thank-you
page (start() once mounted). State in, actions out — exactly the hooks’ contracts above;DonateForm.tsx contract: the controls the options allow, buttonLabel on the
CTA, disabled with “Redirecting…” while submitting, messages once showErrors, error inline,
the closed state when available is false.Routes /donate, /donate/:id (via fetchCampaign, null → your 404), /donate/thank-you; dev server
on 4321; a static build goes through npx @wix/cli@latest release with site.outputDirectory pointing
at the build folder, an SSR build is hosted by you. Page title from the DTO’s name; the description
is your copy — campaigns carry no seoData.
Read static.md in this folder: it is the whole playbook for this stack, in one file. The pages
import the layer from the site’s js/wix/ folder and write every control on the stores;
static.md shows how to drive each store, with the routes, the rules and the seed.
Read the reference files listed above before writing any surface.
Run deploy.mjs donations --stack static in the project folder anyway: js/wix/ is both the
browser-side code and the readable spec. Then split by where the call runs. Reads on the server:
port js/wix/campaigns.ts and donations-core.ts to your language — the same functions returning
the same DTO shapes as dicts (the literal bodies are in the file), one anonymous visitor token per
process for these public reads (mint and refresh per client.ts) — and render the index and the
campaign page in your templates to the contracts above, so campaign names and goals are in the HTML.
Donating in the browser: the form on ./js/wix/donation-store.js (pass the campaign DTO rendered
into the page as JSON), exactly as the static wiring above — the checkout and the redirect session are
created by the visitor’s own token from the page, so the server never handles per-visitor tokens; the
thank-you page mounts createReceiptStore() the same way. Routes stay /donate, /donate/<id>,
/donate/thank-you. Add your public https origin to the OAuth app’s allowed domains before the hosted
checkout can return.
Pre-rendered (Frozen-Flask, Pelican, any static-site generator) → Wix-hosted. Same port for the
reads, run at build time with one anonymous token; the generator emits /donate and one page per
campaign from fetchCampaigns() (at most 100 — one call). Goal progress is frozen at build time —
say so to the owner, or render GoalProgress client-side from a fetchCampaign on load. Run
deploy.mjs donations --stack static --out <build dir> so js/wix/ is inside the output the pages
import from, point site.outputDirectory at that folder, wix release. Pages sit at different depths
(/, /donate/…): give the templates one base path to js/wix/ (root-relative /js/wix/…), never a
relative ./js/wix/. The frozen page is the first paint; the form still runs client-side through
createDonationStore(). Close with the live URL, the rebuild + release command, and one line for the
owner: dashboard edits to campaigns reach the site when that command runs; donating is live regardless.
Import ./styles/global.css once at the app entry (needs @tailwindcss/vite in the vite plugins —
deploy added the dep). Routes: /donate → your grid (useCampaigns() fetches client-side when no
initialCampaigns is passed); /donate/:id → fetchCampaign(id) client-side, then your page around
<DonateForm campaign={c} /> (null → your not-found state); /donate/thank-you → <DonationReceipt />.
Deploy wrote the public client id into wix/config.ts; nothing else to configure.
Routes on Wix hosting: the host serves files only, so a clean route answers 404 when loaded directly —
hash routes, or one HTML file per route, decided before the first route is written; any URL handed to
Wix as a return target must be one the host serves (SKILL.md step 1) — pass it through paths.
DonateForm / useDonation / donationCheckoutUrl
own the checkout line and the hosted redirect session. Never hand-build a checkout URL, never create
an order, never mark anything paid.postFlowUrl (the campaign page) is
hit on abandon too; landing on /donate/thank-you?orderId= is the success redirect. The thank-you
page reads the order when it can and degrades to a thank-you without order facts otherwise — never
an invented amount, name or “paid”.label, target, raised, amount come pre-formatted from the API;
the only computed money is the fee preview — Wix’s fixed 2.9% — and the button total. Wix prices
the line, tax and schedule at the hosted checkout.goal; a campaign without one has none.DonateForm (on static, the page’s own markup on the donation store) — it alone decides which
controls appear (frequencies, presets, custom amount, fee, note) and when a campaign is closed; a
page never rebuilds that.@theme tokens, and your markup
uses Tailwind utilities on the same tokens — one design system across shipped and written code. No
parallel theme files, no hardcoded palette values. Where they don’t (lib, static, a port): style
with whatever your stack does well, on one token set of your own.Before calling this build working, follow the bounded anonymous API smoke test in
How to Code Donations (opens in a new tab):
campaign read → one Cart V2 → calculated line/totals → redirect session, using the same visitor.
Use the sibling file when installed, otherwise
https://www.wix.com/skills/wix-headless/references/inline-recipes/how-to-code-donations.md.
Do not add a V1 Create Checkout step. A local test or a session URL is not proof of payment. Report
API, hosted-checkout and payment verification separately; no payment without explicit authorization.
For ITEM_NOT_FOUND_IN_CATALOG, inspect numeric amount, app/campaign IDs and frequency first, not
reinstallation or premium. Keep sanitized error code/body/stage for diagnosis; do not swallow failures.
A donor’s history or receipts page (member-only order reads), managing a recurring donation (Wix’s
emails and hosted member flows do that), campaign creation or editing from the site (owner-only —
server-side per templates/shared/CUSTOM_OPERATIONS.md, or the dashboard). If the user asks for a
donor account area, deploy the members vertical beside this one rather than shipping code that
returns nothing for visitors.
Hand the owner these links — {siteId} is siteId in wix.config.json (the deploy JSON prints it as
dashboardUrl).
| page | https://manage.wix.com/dashboard/{siteId}/ + |
|---|---|
| Donations (campaigns, settings) | app/333b456e-dd48-4d6b-b32b-9fd48d74e163 (redirects to the app’s own route) |
| Orders — every donation is an eCom order | ecom-platform/orders-list |
| Accept payments — connect a payment method | wix-cashier/payments |
| Upgrade the plan — online payments need premium | full URL: https://www.wix.com/upgrade/website?metaSiteId={siteId} |
Taking real donations needs a connected payment method plus a payments-capable plan. Cart/session
creation can succeed before these are ready; it does not prove a charge can succeed. Do not use a
premium explanation for a malformed catalog reference. A visitor can encounter an unavailable-payments
message at hosted checkout until setup is complete. Recurring frequencies (WEEK/MONTH/YEAR) additionally need a
payment provider that supports recurring payments. Mention it; don’t treat it as a code failure. The
cover-fee 2.9% is added to every recurring charge.
Per seed/SEED.md — plain-data plan.json into seed-donations.mjs from the project root. Seed one
approved cause for a new setup unless the brief lists several. Do not invent impact claims, goals or
deadlines to exercise every form branch, and never reseed for a frontend-only request. Optional form
states can be tested in local fixtures. Ongoing edits use the shared management recipe linked there.
The shipped code guards each of these, so a refusal degrades instead of breaking; confirm them on the first real site and read the owning file if one bites.
/metrics is
unverified. Metrics are caught per campaign: a refusal leaves goal.raised “” (the target still
shows) and options.currency “” (the button reads “Donate” without an amount; preset labels still
carry the API’s own formatted amounts). If the campaign query itself is refused, the pages render
their empty state.donationCheckoutUrl creates one fresh cart with the
donation line (cart.source.channelType: WEB, the donor note as cart.note) and hands that cart’s
id straight to the redirect session — the cart is the checkout, so there is no separate Create
Checkout call and nothing merges into a storefront visitor’s current cart.GET /ecom/v1/orders/{id} with the visitor’s token is
unverified; any failure → receipt null and the page thanks without order facts.orderId spelling. The redirect-session contract appends orderId; the Wix widget’s own success
URL uses orderid. Both are read.app/<appDefId> link redirects to the app’s slug route; the slug itself
was not resolved live.donorCoveringFees flag, 2.9% and one-time frequency omission are documented in the public
donation eCommerce integration (opens in a new tab).