Setting the file. One moment.
Wix Donations · Wix Vibe Headless · wix/skills · Skills Docs
ContentsBack to the top of the page 46.10
Post Detail
Next
Reference INSTRUCTIONS
references/donations/app/rest/ wix-donations.js
JavaScript · 112 lines · 6 KB
RECURRING
=
[
'WEEK'
,
'MONTH'
,
'YEAR'
];
10 const request = ( path , options = {}) => {
11 if (wixClient. WIX_API_SUPPORTS_STRICT_PAYMENT_ERRORS !== true ) {
12 const error = new Error ( 'Donations needs the updated Wix transport. Re-run the Donations deploy step; for a customized src/rest/wix-client.js, merge strict payment-error support from the shared transport before retrying.' );
13 error.code = 'DONATIONS_TRANSPORT_UPGRADE_REQUIRED' ;
14 throw error;
15 }
16 return wixClient. wixApiRequest (path, { ... options, throwOnPaymentRequired: true });
17 };
18
19 /** Raw REST campaigns; page until nextCursor is null. Failures never become an empty result. */
20 export async function queryDonationCampaigns ({ limit = 100 , cursor } = {}) {
21 const data = await request ( `${ CAMPAIGNS }/query` , { body: {
22 query: { filter: { archived: false }, cursorPaging: { limit, ... (cursor ? { cursor } : {}) } },
23 } });
24 return { campaigns: data.donationCampaigns ?? [], nextCursor: data.pagingMetadata?.cursors?.next ?? null };
25 }
26
27 /** null only for an absent/archived campaign; authorization and transport errors propagate. */
28 export async function getDonationCampaign ( id ) {
29 try {
30 const { donationCampaign } = await request ( `${ CAMPAIGNS }/${ encodeURIComponent ( id ) }` , { method: 'GET' });
31 return donationCampaign && ! donationCampaign.archived ? donationCampaign : null ;
32 } catch (error) {
33 if (error.status === 404 ) return null ;
34 throw error;
35 }
36 }
37
38 /** Call only for a campaign with a goal. A failed read must not be displayed as zero fundraising. */
39 export async function getDonationCampaignMetrics ( id ) {
40 const data = await request ( `${ CAMPAIGNS }/${ encodeURIComponent ( id ) }/metrics` , { method: 'GET' });
41 return data.currencyMetricsList ?? [];
42 }
43
44 /** Pure, independently testable conversion from a live campaign and donor selection to Cart V2. */
45 export function donationCartBody ( campaign , { amount : chosen , frequency = 'ONE_TIME' , coverFee = false , note = '' } = {}) {
46 if ( ! campaign?.id || campaign.archived || campaign.status !== 'COLLECTING' ) {
47 throw new Error ( 'This campaign is not accepting donations.' );
48 }
49 if (( typeof chosen !== 'string' && typeof chosen !== 'number' ) || String (chosen). trim () === '' ) {
50 throw new Error ( 'Enter a positive donation amount.' );
51 }
52 const amount = Number (chosen);
53 if ( ! Number. isFinite (amount) || amount <= 0 ) throw new Error ( 'Enter a positive donation amount.' );
54 if ( ! [ 'ONE_TIME' , ... RECURRING ]. includes (frequency) || ! campaign.donationFrequencies?. includes (frequency)) {
55 throw new Error ( 'Choose a frequency offered by this campaign.' );
56 }
57 const isPreset = campaign.predefinedDonationAmounts?. some ( preset => Number (preset.price?.amount) === amount);
58 if ( ! isPreset) {
59 if ( ! campaign.customAmountEnabled) throw new Error ( 'Choose one of the suggested amounts.' );
60 const { minimum , maximum } = campaign.customAmountOptions ?? {};
61 if (minimum && amount < Number (minimum.amount)) throw new Error ( `The minimum donation is ${ minimum . formattedAmount ?? minimum . amount }.` );
62 if (maximum && amount > Number (maximum.amount)) throw new Error ( `The maximum donation is ${ maximum . formattedAmount ?? maximum . amount }.` );
63 }
64 if (coverFee && ! campaign.askDonorCoverFee) throw new Error ( 'Fee coverage is not offered by this campaign.' );
65 const message = String (note). trim ();
66 if (message && ! campaign.commentsEnabled) throw new Error ( 'Notes are not enabled for this campaign.' );
67 if (message. length > 1000 ) throw new Error ( 'Keep your note within 1,000 characters.' );
68 return {
69 cart: { source: { channelType: 'WEB' }, ... (message ? { note: message } : {}) },
70 catalogItems: [{ quantity: 1 , catalogReference: {
71 appId: DONATIONS_APP_ID ,
72 catalogItemId: campaign.id,
73 options: { amount, ... (frequency !== 'ONE_TIME' ? { frequency } : {}), ... (coverFee ? { donorCoveringFees: true } : {}) },
74 } }],
75 };
76 }
77
78 /** Read current settings → one standalone Cart V2 → one hosted session. Never merges the current cart. */
79 export async function donationCheckoutUrl ( campaignId , selection , {
80 origin = typeof window !== 'undefined' ? window.location.origin : '' ,
81 campaignPath = `/donate/${ encodeURIComponent ( campaignId ) }` ,
82 thankYouPath = '/donate/thank-you' ,
83 } = {}) {
84 const site = new URL (origin);
85 if ( ! [ 'https:' , 'http:' ]. includes (site.protocol)) throw new Error ( 'A valid site origin is required.' );
86 const back = new URL (campaignPath, site.origin);
87 const thanks = new URL (thankYouPath, site.origin);
88 if (back.origin !== site.origin || thanks.origin !== site.origin) throw new Error ( 'Checkout callbacks must use this site origin.' );
89 let stage = 'campaign' ;
90 let cartId;
91 try {
92 const campaign = await getDonationCampaign (campaignId);
93 const body = donationCartBody (campaign, selection);
94 stage = 'cart' ;
95 const result = await request ( '/ecom/v2/carts' , { body });
96 cartId = result.cart?.id;
97 if ( ! cartId) throw new Error ( 'Checkout could not start: no cart id returned.' );
98 stage = 'redirect' ;
99 const session = await request ( '/_api/redirects-api/v1/redirect-session' , { body: {
100 ecomCheckout: { checkoutId: cartId },
101 callbacks: { postFlowUrl: back.href, thankYouPageUrl: thanks.href },
102 } });
103 const url = session.redirectSession?.fullUrl;
104 if ( ! url) throw new Error ( 'Checkout could not start: no redirect URL returned.' );
105 return url;
106 } catch (error) {
107 // Preserve the original HTTP status/body; stage and known cart id let the caller diagnose safely.
108 error.stage = stage;
109 if (cartId) error.cartId = cartId;
110 throw error;
111 }
112 }