Setting the file. One moment.
Cart · Wix Headless Templates · wix/skills · Skills Docs
ContentsBack to the top of the page This file
Number 16.822
Position 822 of 828
Type TypeScript
Size 9 KB
Lines 183 storefront/rest/ cart.ts
TypeScript · 183 lines · 9 KB
;
11 import { EMPTY_TOTALS, WIX_STORES_APP_ID, addOptions, assertAdded, assertCheckoutable, cartErrorMessage, rawId, summaryTotals, toCart } from "../app/wix/storefront/cart-core.js" ;
12 import type { Raw } from "../app/wix/storefront/catalog-core.js" ;
13 import type { Cart } from "../app/wix/storefront/types.js" ;
14
15 const CART = "/ecom/v2/carts/current" ;
16
17 export interface AddToCartExtras {
18 /** The resolved variant's option selections: option key → choice key (sent beside variantId, as Wix's own storefront does). */
19 optionChoices ?: Record < string , string >;
20 /** Choice modifiers: modifier key → chosen choice key. */
21 modifierChoices ?: Record < string , string >;
22 /** Free-text modifiers: the modifier's free-text key → the buyer's text. */
23 customTextFields ?: Record < string , string >;
24 /** A chosen recurring plan (SubscriptionPlan.id); omit for a one-time purchase. */
25 subscriptionOptionId ?: string ;
26 /** The resolved variant is out of stock but pre-orderable → preOrderRequested. */
27 preorder ?: boolean ;
28 }
29
30 // Every cart call fails with buyer copy (cart-core maps Wix's codes; the rest keep their message).
31 async function loud < T >( op : () => Promise < T >) : Promise < T > {
32 try {
33 return await op ();
34 } catch (e) {
35 throw new Error ( cartErrorMessage (e));
36 }
37 }
38
39 // A line's attributes.image comes back absent right after an add (verified live) — join the
40 // products' main images by catalogItemId, once per product, cached for the session.
41 // POST /stores/v3/products/query { query: { filter: { id: { $in: [...] } } } }
42 const lineImageCache = new Map < string , string >();
43 async function fillLineImages ( cart : Cart , raws : Raw []) : Promise < void > {
44 const wanted = new Map < string , number []>();
45 raws. forEach (( raw , i ) => {
46 if (cart.lines[i].imageUrl) return ;
47 const pid = raw.source?.catalogReference?.catalogItemId;
48 if ( ! pid) return ;
49 const cached = lineImageCache. get (pid);
50 if (cached !== undefined ) { cart.lines[i].imageUrl = cached; return ; }
51 wanted. set (pid, [ ... (wanted. get (pid) ?? []), i]);
52 });
53 if ( ! wanted.size) return ;
54 try {
55 const res = await wixRequest < Raw >( "/stores/v3/products/query" , { body: { query: { filter: { id: { $in: [ ... wanted. keys ()] } }, cursorPaging: { limit: 100 } } } });
56 for ( const p of (res?.products ?? []) as Raw []) {
57 const url = imgSrc (p.media?.main, 300 , 300 );
58 lineImageCache. set ( rawId (p), url);
59 for ( const i of wanted. get ( rawId (p)) ?? []) cart.lines[i].imageUrl = url;
60 }
61 } catch {
62 /* images are a nicety — the cart stays correct without them */
63 }
64 }
65
66 async function readCart ( raw : Raw | null ) : Promise < Cart > {
67 // Only estimate a cart WITH lines — on an absent/empty cart the endpoint returns 404.
68 if ( ! raw?.lineItems?. length ) return toCart (raw, EMPTY_TOTALS , imgSrc);
69 // The totals come from the estimate, never from hand-summing lines: the after-discount subtotal,
70 // named discounts, fees and taxes (calculated when asked), the total before delivery. Delivery
71 // resolves at checkout — never render it as "Free" here.
72 // POST /ecom/v2/carts/current/estimate { calculateTax, calculateAdditionalFees }
73 let totals = EMPTY_TOTALS ;
74 try {
75 totals = summaryTotals ( await wixRequest < Raw >( `${ CART }/estimate` , { body: { calculateTax: true , calculateAdditionalFees: true } }), raw);
76 } catch {
77 /* the estimate is a display nicety — the cart itself is still valid */
78 }
79 const cart = toCart (raw, totals, imgSrc);
80 await fillLineImages (cart, raw.lineItems as Raw []);
81 return cart;
82 }
83
84 // GET /ecom/v2/carts/current — a 404 (CURRENT_CART_NOT_FOUND / OWNED_CART_NOT_FOUND) means no cart yet.
85 async function getCurrentCartRaw () : Promise < Raw | null > {
86 try {
87 return ( await wixRequest < Raw >( CART , { method: "GET" }))?.cart ?? null ;
88 } catch (e) {
89 if (e instanceof WixApiError && e.status === 404 ) return null ;
90 throw e;
91 }
92 }
93
94 /** The visitor's current cart. An empty Cart (not an error) when none exists yet. */
95 export async function fetchCart () : Promise < Cart > {
96 return readCart ( await getCurrentCartRaw ());
97 }
98
99 /**
100 * Add a product. `variantId` is REQUIRED for any product with options — resolve it first
101 * (resolveVariant); a refused add still returns 200, so the returned line is checked.
102 * POST /ecom/v2/carts/current/add-line-items { catalogItems: [{ quantity, catalogReference: { appId, catalogItemId, options } }] }
103 */
104 export async function addToCart ( productId : string , variantId ?: string | null , quantity = 1 , extras : AddToCartExtras = {}) : Promise < Cart > {
105 return loud ( async () => {
106 const options = addOptions ({ variantId, ... extras });
107 const res = await wixRequest < Raw >( `${ CART }/add-line-items` , {
108 body: { catalogItems: [{ quantity, catalogReference: { catalogItemId: productId, appId: WIX_STORES_APP_ID , ... (Object. keys (options). length ? { options } : {}) } }] },
109 });
110 assertAdded (res?.cart, productId, variantId);
111 return readCart (res?.cart ?? null );
112 });
113 }
114
115 /**
116 * Change a line's quantity. `lineItemId` is CartLine.lineItemId, never the product id; a flat
117 * { id, quantity } is a 400. POST /ecom/v2/carts/current/update-line-items { lineItems: [{ lineItemId, quantity: { newQuantity } }] }
118 */
119 export async function updateQuantity ( lineItemId : string , quantity : number ) : Promise < Cart > {
120 return loud ( async () => {
121 const res = await wixRequest < Raw >( `${ CART }/update-line-items` , { body: { lineItems: [{ lineItemId, quantity: { newQuantity: quantity } }] } });
122 return readCart (res?.cart ?? null );
123 });
124 }
125
126 /** Remove a line by CartLine.lineItemId. POST /ecom/v2/carts/current/remove-line-items { lineItemIds } */
127 export async function removeLine ( lineItemId : string ) : Promise < Cart > {
128 return loud ( async () => {
129 const res = await wixRequest < Raw >( `${ CART }/remove-line-items` , { body: { lineItemIds: [lineItemId] } });
130 return readCart (res?.cart ?? null );
131 });
132 }
133
134 /**
135 * Apply a coupon code (one per cart); buyer copy for an unknown, expired, or inapplicable code.
136 * POST /ecom/v2/carts/current/add-coupon { coupon: { code } }
137 */
138 export async function applyCoupon ( code : string ) : Promise < Cart > {
139 return loud ( async () => {
140 const res = await wixRequest < Raw >( `${ CART }/add-coupon` , { body: { coupon: { code: code. trim () } } });
141 return readCart (res?.cart ?? null );
142 });
143 }
144
145 /** Remove the applied coupon (a no-op when none). POST /ecom/v2/carts/current/remove-coupon { couponId } */
146 export async function removeCoupon () : Promise < Cart > {
147 return loud ( async () => {
148 const current = await getCurrentCartRaw ();
149 const couponId = rawId ((current?.coupons ?? [])[ 0 ]);
150 if ( ! couponId) return readCart (current);
151 const res = await wixRequest < Raw >( `${ CART }/remove-coupon` , { body: { couponId } });
152 return readCart (res?.cart ?? null );
153 });
154 }
155
156 /**
157 * Save the buyer's note to the merchant on the cart (it travels to checkout and the order).
158 * PATCH /ecom/v2/carts/current { cart: { note } }
159 */
160 export async function setNote ( note : string ) : Promise < Cart > {
161 return loud ( async () => {
162 const res = await wixRequest < Raw >( CART , { method: "PATCH" , body: { cart: { note: note. trim () || null } } });
163 return readCart (res?.cart ?? null );
164 });
165 }
166
167 /**
168 * Start the Wix-hosted checkout and return the URL to navigate the FULL document to. The cart's id
169 * IS the checkout id (as on Wix's own storefront). `origin` must be the site's real https origin as
170 * registered on the OAuth app's allowed domains (browser: window.location.origin; server: the
171 * public https host, never the request's internal origin) — an unlisted or http origin 403s on return.
172 * POST /headless/v1/redirect-session { ecomCheckout: { checkoutId }, callbacks: { postFlowUrl, thankYouPageUrl } }
173 */
174 export async function checkoutUrl ( origin : string = typeof window !== "undefined" ? window.location.origin : "" ) : Promise < string > {
175 const raw = await getCurrentCartRaw ();
176 assertCheckoutable (raw);
177 const res = await wixRequest < Raw >( "/headless/v1/redirect-session" , {
178 body: { ecomCheckout: { checkoutId: rawId (raw) }, callbacks: origin ? { postFlowUrl: `${ origin }/` , thankYouPageUrl: `${ origin }/` } : {} },
179 });
180 const url = res?.redirectSession?.fullUrl;
181 if ( ! url) throw new Error ( "Checkout couldn't start: no redirect URL returned." );
182 return url;
183 }