← Files WixARCHIVED FILE

skills/wix-headless-templates/donations/project/src/wix/donations/campaigns.ts

4.47 KB · Oct 8, 2026 · 12:02 UTC

↓ Download file

See the change to this file →

// Campaign reads (Wix Donations, Donation Campaigns) over the SDK — the only file that touches raw
// campaign entities on this transport. Everything it returns is a plain DTO from ./types. The rules
// and mappers live in ./donations-core (shared with the REST twin in templates/donations/rest/);
// this file is the transport only. Copy as-is; extend by adding functions, not by editing these.
//
// The metrics read (raised total, donation count, the site currency) is guarded: a refusal for the
// visitor's token leaves `goal.raised` "" and `options.currency` "" — the page still renders from
// the campaign's own formatted amounts. Whether an anonymous visitor may read metrics is not yet
// verified against a live site.
// docs: https://dev.wix.com/docs/api-reference/business-solutions/donations/donation-campaigns/query-donation-campaigns.md
// docs: https://dev.wix.com/docs/api-reference/business-solutions/donations/donation-campaigns/get-donation-campaign-metrics.md
import { donationCampaigns } from "@wix/donations";
import { ecommerceSettings } from "@wix/ecom";
import { wixModule } from "../sdk";
import { imgSrc } from "../media";
import { currencyOf, isArchived, rawId, toDetail, toSummary, type Raw } from "./donations-core";
import type { CampaignDetail, CampaignSummary } from "./types";

const campaignsApi = wixModule(donationCampaigns);
const settingsApi = wixModule(ecommerceSettings);

// The metrics list is EMPTY on a campaign nobody has donated to yet, and the campaign entity carries
// formatted amounts but no currency code — so a fresh site would format nothing. The site's currency
// comes from the eCommerce settings then (BUSINESS_INFO), read once per process; "" when refused.
// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/ecommerce-settings/get-ecommerce-settings.md
let siteCurrencyPromise: Promise<string> | null = null;
function siteCurrency(): Promise<string> {
  siteCurrencyPromise ??= settingsApi
    .getEcommerceSettings(["BUSINESS_INFO"])
    .then((res: Raw) => String(res?.ecommerceSettings?.businessInfo?.currency ?? ""))
    .catch(() => { siteCurrencyPromise = null; return ""; });
  return siteCurrencyPromise;
}

/** The campaign's currency: the metrics' when a donation exists, else the site's. */
async function currencyFor(metrics: Raw[]): Promise<string> {
  return currencyOf(metrics) || (await siteCurrency());
}

async function metricsOf(campaignId: string): Promise<Raw[]> {
  try {
    const res: Raw = await campaignsApi.getDonationCampaignMetrics(campaignId);
    return (res?.currencyMetricsList ?? []) as Raw[];
  } catch {
    return []; // metrics are a nicety — the campaign stays correct without them
  }
}

const isNotFound = (e: unknown): boolean => {
  const err = e as Raw;
  const code = err?.details?.applicationError?.code ?? err?.code;
  const msg = String(err?.message ?? "");
  return code === "NOT_FOUND" || err?.status === 404 || /not.?found|404/i.test(msg);
};

/** List the non-archived campaigns, oldest first, as card-ready DTOs (goal progress joined per campaign). */
export async function fetchCampaigns({ limit = 100 } = {}): Promise<CampaignSummary[]> {
  const res = await campaignsApi.queryDonationCampaigns().eq("archived", false).ascending("_createdDate").limit(limit).find();
  const items = ((res.items ?? []) as Raw[]).filter((raw) => !isArchived(raw));
  const metrics = await Promise.all(items.map((raw) => (raw.campaignGoal ? metricsOf(rawId(raw)) : Promise.resolve([] as Raw[]))));
  const currencies = await Promise.all(metrics.map(currencyFor));
  return items.map((raw, i) => toSummary(raw, metrics[i], currencies[i], imgSrc));
}

/** One campaign by id with its goal progress and form options. Null when not found or archived. */
export async function fetchCampaign(campaignId: string): Promise<CampaignDetail | null> {
  let raw: Raw | undefined;
  try {
    raw = (await campaignsApi.getDonationCampaign(campaignId)) as Raw;
  } catch (e) {
    if (isNotFound(e)) return null;
    throw e;
  }
  if (!raw || isArchived(raw)) return null; // an archived campaign is reachable by id but never shows a form
  const metrics = await metricsOf(campaignId);
  return toDetail(raw, metrics, await currencyFor(metrics), imgSrc);
}

/** The Wix widget's default: the first campaign by creation date. Null when the site has none. */
export async function fetchDefaultCampaign(): Promise<CampaignDetail | null> {
  const [first] = await fetchCampaigns({ limit: 1 });
  return first ? fetchCampaign(first.id) : null;
}

SHA-256: a8128eae41b71857a28212b05e126fc2324f97b8bb4006bbe841889f2d98b038