@behindthescenes/cart 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,413 @@
1
+ # `@behindthescenes/cart`
2
+
3
+ Browser SDK for BTS external-site cart state and checkout-link handoff. Use it to persist selected products, validate availability with the BTS API, and send shoppers to static or dynamic BTS checkout links.
4
+
5
+ ## Overview
6
+
7
+ The BTS Cart SDK provides:
8
+
9
+ - **Persistent cart state** - Selected products and quantities are stored in browser `localStorage` by default.
10
+ - **Dynamic checkout links** - Generates BTS `/c/:checkoutLinkId?cart=...` URLs from `{ lineItemId, quantity, discount }` cart items.
11
+ - **Static checkout links** - Supports one-click static checkout links through the same SDK API.
12
+ - **Multiple checkout links** - Configure named checkout links and choose which one to use at checkout.
13
+ - **Site-key protection** - Cart URL generation is validated by the BTS website cart API using the same public site key and verified-domain model as `@behindthescenes/analytics`.
14
+ - **Availability validation** - The API validates checkout link status, products/access tiers, requested quantity, and discounts before returning a checkout URL.
15
+
16
+ ## Installation
17
+
18
+ ### npm/yarn/pnpm/bun
19
+
20
+ ```bash
21
+ npm install @behindthescenes/cart
22
+ ```
23
+
24
+ ```bash
25
+ pnpm add @behindthescenes/cart
26
+ ```
27
+
28
+ ```bash
29
+ yarn add @behindthescenes/cart
30
+ ```
31
+
32
+ ```bash
33
+ bun add @behindthescenes/cart
34
+ ```
35
+
36
+ ### Browser Bundle (Hosted)
37
+
38
+ ```html
39
+ <script type="module">
40
+ import { createBTSCart } from "https://behindthescenes.com/sdk/@behindthescenes/cart/latest/browser/browser.js";
41
+
42
+ window.btsCart = createBTSCart({
43
+ siteKey: "your-public-site-key",
44
+ checkoutLinks: {
45
+ tickets: {
46
+ checkoutLinkId: "your-checkout-link-id",
47
+ mode: "dynamic"
48
+ }
49
+ },
50
+ defaultCheckoutLinkKey: "tickets"
51
+ });
52
+ </script>
53
+ ```
54
+
55
+ ## Quick Start
56
+
57
+ ```ts
58
+ import { createBTSCart } from "@behindthescenes/cart";
59
+
60
+ const cart = createBTSCart({
61
+ siteKey: "your-public-site-key",
62
+ checkoutLinks: {
63
+ tickets: {
64
+ checkoutLinkId: "https://dev.behindthescenes.com/c/1a9d4f91-c1dd-455d-aa4c-851b296aff75",
65
+ mode: "dynamic",
66
+ },
67
+ },
68
+ defaultCheckoutLinkKey: "tickets",
69
+ });
70
+
71
+ cart.addItem({
72
+ id: "garnet",
73
+ lineItemId: "2dd3b989-616e-4e2f-9c47-21e1288d3823",
74
+ checkoutLinkKey: "tickets",
75
+ name: "Garnet",
76
+ unitPrice: 395,
77
+ fullPrice: 550,
78
+ quantity: 1,
79
+ discount: "GARNET-EARLYBIRD",
80
+ });
81
+
82
+ await cart.checkout("tickets");
83
+ ```
84
+
85
+ ## Setup in BTS
86
+
87
+ Before using the SDK, configure the destination site in BTS:
88
+
89
+ 1. In BTS, go to the relevant space.
90
+ 2. Configure the external website integration and copy the public site key.
91
+ 3. Verify the external site domain for production use.
92
+ 4. Create a checkout link.
93
+ 5. For dynamic checkout links, use either the checkout link ID or the full checkout link URL plus the relevant product/access-tier line item IDs.
94
+
95
+ Local development can use localhost origins without verified-domain matching when the API runs in a local/development/test environment.
96
+
97
+ ## Dynamic Checkout Links
98
+
99
+ Dynamic checkout links use the BTS checkout cart query parameter:
100
+
101
+ ```ts
102
+ encodeURIComponent(
103
+ JSON.stringify([
104
+ {
105
+ lineItemId: "2dd3b989-616e-4e2f-9c47-21e1288d3823",
106
+ quantity: 1,
107
+ discount: "c16cfeea-c604-4009-851c-05e6dcdfbcea",
108
+ },
109
+ ]),
110
+ );
111
+ ```
112
+
113
+ The SDK sends that cart payload to the BTS API for validation and receives a final checkout URL. The returned URL is equivalent to:
114
+
115
+ ```text
116
+ https://behindthescenes.com/c/<checkoutLinkId>?cart=<encoded-json>
117
+ ```
118
+
119
+ For dynamic checkout links with no saved checkout-link line items, BTS resolves valid line items from the checkout link space’s public products and access tiers. In that case, `lineItemId` is the product ID or access tier ID.
120
+
121
+ `discount` can be either the internal promo-code ID or the customer-facing promo code. The cart API resolves customer-facing codes to the internal IDs required by the BTS checkout page before returning the final URL.
122
+
123
+ Checkout link config also accepts full BTS checkout URLs:
124
+
125
+ ```ts
126
+ const cart = createBTSCart({
127
+ siteKey: "your-public-site-key",
128
+ checkoutLinks: {
129
+ tickets: {
130
+ checkoutLinkId: "https://behindthescenes.com/c/1a9d4f91-c1dd-455d-aa4c-851b296aff75?discountCode=EARLYBIRD",
131
+ mode: "dynamic",
132
+ },
133
+ },
134
+ });
135
+ ```
136
+
137
+ The SDK extracts the checkout link ID and default `discountCode` from the URL.
138
+
139
+ ## Static Checkout Links
140
+
141
+ ```ts
142
+ const cart = createBTSCart({
143
+ siteKey: "your-public-site-key",
144
+ checkoutLinks: {
145
+ vip: {
146
+ checkoutLinkId: "static-checkout-link-id",
147
+ mode: "static",
148
+ discountCode: "VIP-EARLYBIRD",
149
+ },
150
+ },
151
+ });
152
+
153
+ await cart.checkout("vip");
154
+ ```
155
+
156
+ Static links ignore cart items and validate the configured checkout link before redirecting.
157
+
158
+ ## Multiple Checkout Links
159
+
160
+ ```ts
161
+ const cart = createBTSCart({
162
+ siteKey: "your-public-site-key",
163
+ checkoutLinks: {
164
+ tickets: {
165
+ checkoutLinkId: "dynamic-ticket-link-id",
166
+ mode: "dynamic",
167
+ },
168
+ merch: {
169
+ checkoutLinkId: "dynamic-merch-link-id",
170
+ mode: "dynamic",
171
+ },
172
+ },
173
+ defaultCheckoutLinkKey: "tickets",
174
+ });
175
+
176
+ cart.addItem({
177
+ id: "shirt",
178
+ lineItemId: "product-line-item-id",
179
+ checkoutLinkKey: "merch",
180
+ name: "Event Shirt",
181
+ unitPrice: 49,
182
+ });
183
+
184
+ await cart.checkout("merch");
185
+ ```
186
+
187
+ ## Persistence
188
+
189
+ The SDK persists cart state to `localStorage` by default.
190
+
191
+ ```ts
192
+ const cart = createBTSCart({
193
+ siteKey: "your-public-site-key",
194
+ storageKey: "my-site-cart",
195
+ persist: true,
196
+ });
197
+ ```
198
+
199
+ Disable persistence:
200
+
201
+ ```ts
202
+ const cart = createBTSCart({
203
+ siteKey: "your-public-site-key",
204
+ persist: false,
205
+ });
206
+ ```
207
+
208
+ Provide a custom storage adapter:
209
+
210
+ ```ts
211
+ const cart = createBTSCart({
212
+ siteKey: "your-public-site-key",
213
+ storage: {
214
+ getItem: (key) => sessionStorage.getItem(key),
215
+ setItem: (key, value) => sessionStorage.setItem(key, value),
216
+ removeItem: (key) => sessionStorage.removeItem(key),
217
+ },
218
+ });
219
+ ```
220
+
221
+ ## State API
222
+
223
+ ```ts
224
+ cart.addItem(item);
225
+ cart.removeItem("garnet");
226
+ cart.setQuantity("garnet", 2);
227
+ cart.increment("garnet");
228
+ cart.decrement("garnet");
229
+ cart.clear();
230
+
231
+ const items = cart.getItems();
232
+ const itemCount = cart.getItemCount();
233
+ const subtotal = cart.getSubtotal();
234
+ const total = cart.getTotal();
235
+ ```
236
+
237
+ Subscribe to changes:
238
+
239
+ ```ts
240
+ const unsubscribe = cart.subscribe((state) => {
241
+ console.log(state.items);
242
+ });
243
+
244
+ unsubscribe();
245
+ ```
246
+
247
+ ## Checkout API
248
+
249
+ Return a checkout URL without redirecting:
250
+
251
+ ```ts
252
+ const url = await cart.resolveCheckoutUrl("tickets");
253
+ ```
254
+
255
+ Validate and redirect:
256
+
257
+ ```ts
258
+ await cart.checkout("tickets");
259
+ ```
260
+
261
+ ## React Integration
262
+
263
+ ```tsx
264
+ import { createBTSCart, type BTSCart, type BTSCartItem } from "@behindthescenes/cart";
265
+ import { createContext, useContext, useEffect, useMemo, useState } from "react";
266
+
267
+ const CartContext = createContext<BTSCart | null>(null);
268
+
269
+ export function CartProvider({ children }: { children: React.ReactNode }) {
270
+ const cart = useMemo(
271
+ () =>
272
+ createBTSCart({
273
+ siteKey: "your-public-site-key",
274
+ checkoutLinks: {
275
+ tickets: {
276
+ checkoutLinkId: "dynamic-ticket-link-id",
277
+ mode: "dynamic",
278
+ },
279
+ },
280
+ defaultCheckoutLinkKey: "tickets",
281
+ }),
282
+ [],
283
+ );
284
+
285
+ return <CartContext.Provider value={cart}>{children}</CartContext.Provider>;
286
+ }
287
+
288
+ export function useCartItems(): BTSCartItem[] {
289
+ const cart = useContext(CartContext);
290
+ if (!cart) throw new Error("useCartItems must be used inside CartProvider");
291
+
292
+ const [items, setItems] = useState(() => cart.getItems());
293
+
294
+ useEffect(() => cart.subscribe((state) => setItems(state.items)), [cart]);
295
+
296
+ return items;
297
+ }
298
+ ```
299
+
300
+ ## Configuration
301
+
302
+ ```ts
303
+ type BTSCartInit = {
304
+ siteKey: string;
305
+ endpoint?: string;
306
+ checkoutLinks?: Record<string, BTSCartCheckoutLinkConfig>;
307
+ defaultCheckoutLinkKey?: string;
308
+ debug?: boolean;
309
+ persist?: boolean;
310
+ storageKey?: string;
311
+ storage?: BTSCartStorage;
312
+ requestHeaders?: HeadersInit | ((request: BTSCartRequestContext) => HeadersInit | Promise<HeadersInit>);
313
+ };
314
+ ```
315
+
316
+ Defaults:
317
+
318
+ - `endpoint`: `https://api.bts.it.com/v2/website/cart`
319
+ - `persist`: `true`
320
+ - `storageKey`: `bts-cart`
321
+ - `defaultCheckoutLinkKey`: first configured checkout link key, or `default`
322
+
323
+ ## Browser Global API
324
+
325
+ The hosted browser bundle installs:
326
+
327
+ - `window.BTSCart`
328
+ - `window.createBTSCart`
329
+ - `window.btsCart`
330
+ - `window.btsCartDataLayer`
331
+ - `window.btsCartCommand`
332
+
333
+ Queued command example:
334
+
335
+ ```html
336
+ <script>
337
+ window.btsCartDataLayer = window.btsCartDataLayer || [];
338
+ function btsCartCommand(){window.btsCartDataLayer.push(arguments);}
339
+
340
+ btsCartCommand("config", {
341
+ siteKey: "your-public-site-key",
342
+ checkoutLinks: {
343
+ tickets: {
344
+ checkoutLinkId: "dynamic-ticket-link-id",
345
+ mode: "dynamic"
346
+ }
347
+ }
348
+ });
349
+ </script>
350
+ ```
351
+
352
+ ## Development
353
+
354
+ ```bash
355
+ bun install
356
+ bun run type-check
357
+ bun test
358
+ bun run build
359
+ ```
360
+
361
+ Build artifacts:
362
+
363
+ - `dist/esm/index.js`
364
+ - `dist/cjs/index.js`
365
+ - `dist/browser/browser.js`
366
+ - `dist/types/index.d.ts`
367
+ - `dist/manifest.json`
368
+
369
+ ## Hosted Bundle
370
+
371
+ Sync the hosted bundle into `apps/bts-web/public/sdk/@behindthescenes/cart`:
372
+
373
+ ```bash
374
+ bun run build:hosted
375
+ ```
376
+
377
+ Force a rebuild before syncing:
378
+
379
+ ```bash
380
+ bun run build:hosted:force
381
+ ```
382
+
383
+ Hosted layout matches `@behindthescenes/analytics`:
384
+
385
+ - `latest`
386
+ - one major-version directory, for example `0` or `1`
387
+
388
+ ## Release
389
+
390
+ The cart SDK uses the same release pipeline as `@behindthescenes/analytics`.
391
+
392
+ Manual release from GitHub Actions:
393
+
394
+ 1. Open **Cart SDK Release**.
395
+ 2. Choose `patch`, `minor`, or `major`.
396
+ 3. Run the workflow from `main`.
397
+
398
+ The workflow:
399
+
400
+ - installs dependencies with Bun
401
+ - bumps the package version
402
+ - runs `bun run release:check`
403
+ - publishes with `npm publish --provenance --access public`
404
+ - commits the version bump back to `main`
405
+
406
+ Tag release:
407
+
408
+ ```bash
409
+ git tag cart-sdk-v0.0.2
410
+ git push origin cart-sdk-v0.0.2
411
+ ```
412
+
413
+ The tag version is applied to `packages/cart/package.json` before publishing.
@@ -0,0 +1 @@
1
+ var R="https://api.bts.it.com/v2/website/cart",v="bts-cart",F="default",G=1;class S extends Error{code;status;constructor(C,w,f){super(w);this.name="BTSCartError",this.code=C,this.status=f}}function M(){return typeof window>"u"?null:window}function Y(){try{return M()?.localStorage}catch{return}}function Z(C){return(C??R).replace(/\/$/,"")}function _(C){if(!Number.isFinite(C??1))return 1;return Math.max(1,Math.floor(C??1))}function J(C){return{items:C.map((w)=>({...w}))}}function $(C,w){let f=new Headers(C);if(!w)return f;return new Headers(w).forEach((r,B)=>f.set(B,r)),f}function z(C){let w=C.checkoutLinkId.trim();try{let f=new URL(w.includes("://")?w:w.startsWith("/")?`https://behindthescenes.com${w}`:w),r=f.pathname.split("/").filter(Boolean),B=r.indexOf("c"),b=B>=0?r[B+1]:r.at(-1);if(b)return{checkoutLinkId:b,discountCode:C.discountCode??f.searchParams.get("discountCode")??void 0}}catch{}return{checkoutLinkId:w,discountCode:C.discountCode}}class T{siteKey;endpoint;debug;persist;storageKey;storage;checkoutLinks;defaultCheckoutLinkKey;requestHeaders;items=[];listeners=new Set;constructor(C){this.siteKey=C.siteKey,this.endpoint=Z(C.endpoint),this.debug=C.debug??!1,this.persist=C.persist??!0,this.storageKey=C.storageKey??v,this.storage=C.storage??Y(),this.checkoutLinks=C.checkoutLinks??{},this.defaultCheckoutLinkKey=C.defaultCheckoutLinkKey??Object.keys(this.checkoutLinks)[0]??F,this.requestHeaders=C.requestHeaders,this.rehydrate()}static init(C){return new T(C)}getItems(){return J(this.items).items}getState(){return J(this.items)}getItemCount(){return this.items.reduce((C,w)=>C+w.quantity,0)}getSubtotal(){return this.items.reduce((C,w)=>C+(w.fullPrice??w.unitPrice)*w.quantity,0)}getTotal(){return this.items.reduce((C,w)=>C+w.unitPrice*w.quantity,0)}subscribe(C){return this.listeners.add(C),C(this.getState()),()=>{this.listeners.delete(C)}}destroy(){this.listeners.clear()}rehydrate(){if(!this.persist||!this.storage)return;try{let C=this.storage.getItem(this.storageKey);if(!C)return;let w=JSON.parse(C),f=Array.isArray(w)?w:w.items;if(!Array.isArray(f))return;this.items=f.map((r)=>this.normalizeItem(r)).filter((r)=>Boolean(r)),this.emit()}catch(C){this.log("Failed to hydrate cart",C)}}addItem(C){let w=this.normalizeItem(C);if(!w)return;let f=this.items.find((r)=>r.id===w.id);if(f){this.setQuantity(f.id,f.quantity+w.quantity);return}this.items=[...this.items,w],this.commit()}removeItem(C){this.items=this.items.filter((w)=>w.id!==C),this.commit()}setQuantity(C,w){let f=Math.floor(w);this.items=f<=0?this.items.filter((r)=>r.id!==C):this.items.map((r)=>r.id===C?{...r,quantity:f}:r),this.commit()}increment(C,w=1){let f=this.items.find((r)=>r.id===C);if(f)this.setQuantity(C,f.quantity+Math.max(1,Math.floor(w)))}decrement(C,w=1){let f=this.items.find((r)=>r.id===C);if(f)this.setQuantity(C,f.quantity-Math.max(1,Math.floor(w)))}clear(){if(this.items=[],this.persist&&this.storage)try{this.storage.removeItem(this.storageKey)}catch(C){this.log("Failed to clear cart storage",C)}this.emit()}async resolveCheckoutUrl(C=this.defaultCheckoutLinkKey){let w=this.checkoutLinks[C];if(!w)throw new S("checkout_link_not_found",`Unknown checkout link key: ${C}`);let f=z(w),r=this.itemsForCheckout(C,w);return(await this.post("/checkout-url",{siteKey:this.siteKey,checkoutLinkId:f.checkoutLinkId,mode:w.mode,discountCode:f.discountCode,items:r})).checkoutUrl}async checkout(C=this.defaultCheckoutLinkKey){let w=await this.resolveCheckoutUrl(C),f=M();if(f)f.location.assign(w);return w}itemsForCheckout(C,w){if(w.mode==="static")return[];let f=this.items.filter((r)=>r.checkoutLinkKey===C).map((r)=>({lineItemId:r.lineItemId,quantity:r.quantity,...r.discount?{discount:r.discount}:{}}));if(f.length===0)throw new S("cart_empty","Your cart is empty.");return f}normalizeItem(C){if(!C.id||!C.lineItemId||!C.name||typeof C.unitPrice!=="number")return null;return{...C,id:C.id,lineItemId:C.lineItemId,checkoutLinkKey:C.checkoutLinkKey??this.defaultCheckoutLinkKey,name:C.name,unitPrice:C.unitPrice,quantity:_(C.quantity)}}async post(C,w){let f=`${this.endpoint}${C}`,r=JSON.stringify(w),B={"Content-Type":"application/json"},b={body:w,bodyText:r,endpoint:this.endpoint,headers:B,path:C,siteKey:this.siteKey,url:f},P=typeof this.requestHeaders==="function"?await this.requestHeaders(b):this.requestHeaders,U=$(B,P),D=await fetch(f,{method:"POST",headers:U,body:r}),A=await D.json().catch(()=>null);if(!D.ok){let V=typeof A==="object"&&A&&"code"in A?A.code:"request_failed",X=typeof A==="object"&&A&&"message"in A?A.message:"Cart request failed";throw new S(V??"request_failed",X??"Cart request failed",D.status)}return A}commit(){if(this.persist&&this.storage)try{let C={version:G,items:this.items};this.storage.setItem(this.storageKey,JSON.stringify(C))}catch(C){this.log("Failed to persist cart",C)}this.emit()}emit(){let C=this.getState();for(let w of this.listeners)w(C)}log(...C){if(this.debug)console.log("[@behindthescenes/cart]",...C)}}function I(C){return T.init(C)}function N(C){return typeof C==="object"&&C!==null&&!Array.isArray(C)}function O(C){let w=typeof window>"u"?null:window;if(!w)return;let[f,r,B]=C;if(typeof f!=="string")return;if(f==="config"){if(!N(r))return;w.btsCart?.destroy?.(),w.btsCart=I(r);return}let b=w.btsCart;if(!b)return;if(f==="add"&&N(r)){b.addItem(r);return}if(f==="remove"&&typeof r==="string"){b.removeItem(r);return}if(f==="quantity"&&typeof r==="string"&&typeof B==="number"){b.setQuantity(r,B);return}if(f==="clear"){b.clear();return}if(f==="checkout")b.checkout(typeof r==="string"?r:void 0)}if(typeof window<"u"){let C=Array.isArray(window.btsCartDataLayer)?[...window.btsCartDataLayer]:[];window.btsCartDataLayer=Array.isArray(window.btsCartDataLayer)?window.btsCartDataLayer:[],window.BTSCart={BTSCart:T,createBTSCart:I},window.createBTSCart=I;for(let w of C)O(Array.from(w));window.btsCartCommand=(...w)=>{window.btsCartDataLayer?.push(w),O(w)}}export{I as createBTSCart,T as BTSCart};
@@ -0,0 +1 @@
1
+ var{defineProperty:_,getOwnPropertyNames:H,getOwnPropertyDescriptor:b}=Object,q=Object.prototype.hasOwnProperty;function B(F){return this[F]}var I=(F)=>{var G=($??=new WeakMap).get(F),J;if(G)return G;if(G=_({},"__esModule",{value:!0}),F&&typeof F==="object"||typeof F==="function"){for(var M of H(F))if(!q.call(G,M))_(G,M,{get:B.bind(F,M),enumerable:!(J=b(F,M))||J.enumerable})}return $.set(F,G),G},$;var j=(F)=>F;function K(F,G){this[F]=j.bind(null,G)}var T=(F,G)=>{for(var J in G)_(F,J,{get:G[J],enumerable:!0,configurable:!0,set:K.bind(G,J)})};var g={};T(g,{createBTSCart:()=>f,BTSCartError:()=>V,BTSCart:()=>Y});module.exports=I(g);var A="https://api.bts.it.com/v2/website/cart",P="bts-cart",R="default",v=1;class V extends Error{code;status;constructor(F,G,J){super(G);this.name="BTSCartError",this.code=F,this.status=J}}function D(){return typeof window>"u"?null:window}function C(){try{return D()?.localStorage}catch{return}}function S(F){return(F??A).replace(/\/$/,"")}function w(F){if(!Number.isFinite(F??1))return 1;return Math.max(1,Math.floor(F??1))}function z(F){return{items:F.map((G)=>({...G}))}}function x(F,G){let J=new Headers(F);if(!G)return J;return new Headers(G).forEach((M,O)=>J.set(O,M)),J}function L(F){let G=F.checkoutLinkId.trim();try{let J=new URL(G.includes("://")?G:G.startsWith("/")?`https://behindthescenes.com${G}`:G),M=J.pathname.split("/").filter(Boolean),O=M.indexOf("c"),X=O>=0?M[O+1]:M.at(-1);if(X)return{checkoutLinkId:X,discountCode:F.discountCode??J.searchParams.get("discountCode")??void 0}}catch{}return{checkoutLinkId:G,discountCode:F.discountCode}}class Y{siteKey;endpoint;debug;persist;storageKey;storage;checkoutLinks;defaultCheckoutLinkKey;requestHeaders;items=[];listeners=new Set;constructor(F){this.siteKey=F.siteKey,this.endpoint=S(F.endpoint),this.debug=F.debug??!1,this.persist=F.persist??!0,this.storageKey=F.storageKey??P,this.storage=F.storage??C(),this.checkoutLinks=F.checkoutLinks??{},this.defaultCheckoutLinkKey=F.defaultCheckoutLinkKey??Object.keys(this.checkoutLinks)[0]??R,this.requestHeaders=F.requestHeaders,this.rehydrate()}static init(F){return new Y(F)}getItems(){return z(this.items).items}getState(){return z(this.items)}getItemCount(){return this.items.reduce((F,G)=>F+G.quantity,0)}getSubtotal(){return this.items.reduce((F,G)=>F+(G.fullPrice??G.unitPrice)*G.quantity,0)}getTotal(){return this.items.reduce((F,G)=>F+G.unitPrice*G.quantity,0)}subscribe(F){return this.listeners.add(F),F(this.getState()),()=>{this.listeners.delete(F)}}destroy(){this.listeners.clear()}rehydrate(){if(!this.persist||!this.storage)return;try{let F=this.storage.getItem(this.storageKey);if(!F)return;let G=JSON.parse(F),J=Array.isArray(G)?G:G.items;if(!Array.isArray(J))return;this.items=J.map((M)=>this.normalizeItem(M)).filter((M)=>Boolean(M)),this.emit()}catch(F){this.log("Failed to hydrate cart",F)}}addItem(F){let G=this.normalizeItem(F);if(!G)return;let J=this.items.find((M)=>M.id===G.id);if(J){this.setQuantity(J.id,J.quantity+G.quantity);return}this.items=[...this.items,G],this.commit()}removeItem(F){this.items=this.items.filter((G)=>G.id!==F),this.commit()}setQuantity(F,G){let J=Math.floor(G);this.items=J<=0?this.items.filter((M)=>M.id!==F):this.items.map((M)=>M.id===F?{...M,quantity:J}:M),this.commit()}increment(F,G=1){let J=this.items.find((M)=>M.id===F);if(J)this.setQuantity(F,J.quantity+Math.max(1,Math.floor(G)))}decrement(F,G=1){let J=this.items.find((M)=>M.id===F);if(J)this.setQuantity(F,J.quantity-Math.max(1,Math.floor(G)))}clear(){if(this.items=[],this.persist&&this.storage)try{this.storage.removeItem(this.storageKey)}catch(F){this.log("Failed to clear cart storage",F)}this.emit()}async resolveCheckoutUrl(F=this.defaultCheckoutLinkKey){let G=this.checkoutLinks[F];if(!G)throw new V("checkout_link_not_found",`Unknown checkout link key: ${F}`);let J=L(G),M=this.itemsForCheckout(F,G);return(await this.post("/checkout-url",{siteKey:this.siteKey,checkoutLinkId:J.checkoutLinkId,mode:G.mode,discountCode:J.discountCode,items:M})).checkoutUrl}async checkout(F=this.defaultCheckoutLinkKey){let G=await this.resolveCheckoutUrl(F),J=D();if(J)J.location.assign(G);return G}itemsForCheckout(F,G){if(G.mode==="static")return[];let J=this.items.filter((M)=>M.checkoutLinkKey===F).map((M)=>({lineItemId:M.lineItemId,quantity:M.quantity,...M.discount?{discount:M.discount}:{}}));if(J.length===0)throw new V("cart_empty","Your cart is empty.");return J}normalizeItem(F){if(!F.id||!F.lineItemId||!F.name||typeof F.unitPrice!=="number")return null;return{...F,id:F.id,lineItemId:F.lineItemId,checkoutLinkKey:F.checkoutLinkKey??this.defaultCheckoutLinkKey,name:F.name,unitPrice:F.unitPrice,quantity:w(F.quantity)}}async post(F,G){let J=`${this.endpoint}${F}`,M=JSON.stringify(G),O={"Content-Type":"application/json"},X={body:G,bodyText:M,endpoint:this.endpoint,headers:O,path:F,siteKey:this.siteKey,url:J},U=typeof this.requestHeaders==="function"?await this.requestHeaders(X):this.requestHeaders,W=x(O,U),Z=await fetch(J,{method:"POST",headers:W,body:M}),N=await Z.json().catch(()=>null);if(!Z.ok){let Q=typeof N==="object"&&N&&"code"in N?N.code:"request_failed",E=typeof N==="object"&&N&&"message"in N?N.message:"Cart request failed";throw new V(Q??"request_failed",E??"Cart request failed",Z.status)}return N}commit(){if(this.persist&&this.storage)try{let F={version:v,items:this.items};this.storage.setItem(this.storageKey,JSON.stringify(F))}catch(F){this.log("Failed to persist cart",F)}this.emit()}emit(){let F=this.getState();for(let G of this.listeners)G(F)}log(...F){if(this.debug)console.log("[@behindthescenes/cart]",...F)}}function f(F){return Y.init(F)}
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1 @@
1
+ var _="https://api.bts.it.com/v2/website/cart",$="bts-cart",A="default",P=1;class V extends Error{code;status;constructor(F,G,J){super(G);this.name="BTSCartError",this.code=F,this.status=J}}function v(){return typeof window>"u"?null:window}function Q(){try{return v()?.localStorage}catch{return}}function E(F){return(F??_).replace(/\/$/,"")}function H(F){if(!Number.isFinite(F??1))return 1;return Math.max(1,Math.floor(F??1))}function R(F){return{items:F.map((G)=>({...G}))}}function b(F,G){let J=new Headers(F);if(!G)return J;return new Headers(G).forEach((M,O)=>J.set(O,M)),J}function q(F){let G=F.checkoutLinkId.trim();try{let J=new URL(G.includes("://")?G:G.startsWith("/")?`https://behindthescenes.com${G}`:G),M=J.pathname.split("/").filter(Boolean),O=M.indexOf("c"),X=O>=0?M[O+1]:M.at(-1);if(X)return{checkoutLinkId:X,discountCode:F.discountCode??J.searchParams.get("discountCode")??void 0}}catch{}return{checkoutLinkId:G,discountCode:F.discountCode}}class Z{siteKey;endpoint;debug;persist;storageKey;storage;checkoutLinks;defaultCheckoutLinkKey;requestHeaders;items=[];listeners=new Set;constructor(F){this.siteKey=F.siteKey,this.endpoint=E(F.endpoint),this.debug=F.debug??!1,this.persist=F.persist??!0,this.storageKey=F.storageKey??$,this.storage=F.storage??Q(),this.checkoutLinks=F.checkoutLinks??{},this.defaultCheckoutLinkKey=F.defaultCheckoutLinkKey??Object.keys(this.checkoutLinks)[0]??A,this.requestHeaders=F.requestHeaders,this.rehydrate()}static init(F){return new Z(F)}getItems(){return R(this.items).items}getState(){return R(this.items)}getItemCount(){return this.items.reduce((F,G)=>F+G.quantity,0)}getSubtotal(){return this.items.reduce((F,G)=>F+(G.fullPrice??G.unitPrice)*G.quantity,0)}getTotal(){return this.items.reduce((F,G)=>F+G.unitPrice*G.quantity,0)}subscribe(F){return this.listeners.add(F),F(this.getState()),()=>{this.listeners.delete(F)}}destroy(){this.listeners.clear()}rehydrate(){if(!this.persist||!this.storage)return;try{let F=this.storage.getItem(this.storageKey);if(!F)return;let G=JSON.parse(F),J=Array.isArray(G)?G:G.items;if(!Array.isArray(J))return;this.items=J.map((M)=>this.normalizeItem(M)).filter((M)=>Boolean(M)),this.emit()}catch(F){this.log("Failed to hydrate cart",F)}}addItem(F){let G=this.normalizeItem(F);if(!G)return;let J=this.items.find((M)=>M.id===G.id);if(J){this.setQuantity(J.id,J.quantity+G.quantity);return}this.items=[...this.items,G],this.commit()}removeItem(F){this.items=this.items.filter((G)=>G.id!==F),this.commit()}setQuantity(F,G){let J=Math.floor(G);this.items=J<=0?this.items.filter((M)=>M.id!==F):this.items.map((M)=>M.id===F?{...M,quantity:J}:M),this.commit()}increment(F,G=1){let J=this.items.find((M)=>M.id===F);if(J)this.setQuantity(F,J.quantity+Math.max(1,Math.floor(G)))}decrement(F,G=1){let J=this.items.find((M)=>M.id===F);if(J)this.setQuantity(F,J.quantity-Math.max(1,Math.floor(G)))}clear(){if(this.items=[],this.persist&&this.storage)try{this.storage.removeItem(this.storageKey)}catch(F){this.log("Failed to clear cart storage",F)}this.emit()}async resolveCheckoutUrl(F=this.defaultCheckoutLinkKey){let G=this.checkoutLinks[F];if(!G)throw new V("checkout_link_not_found",`Unknown checkout link key: ${F}`);let J=q(G),M=this.itemsForCheckout(F,G);return(await this.post("/checkout-url",{siteKey:this.siteKey,checkoutLinkId:J.checkoutLinkId,mode:G.mode,discountCode:J.discountCode,items:M})).checkoutUrl}async checkout(F=this.defaultCheckoutLinkKey){let G=await this.resolveCheckoutUrl(F),J=v();if(J)J.location.assign(G);return G}itemsForCheckout(F,G){if(G.mode==="static")return[];let J=this.items.filter((M)=>M.checkoutLinkKey===F).map((M)=>({lineItemId:M.lineItemId,quantity:M.quantity,...M.discount?{discount:M.discount}:{}}));if(J.length===0)throw new V("cart_empty","Your cart is empty.");return J}normalizeItem(F){if(!F.id||!F.lineItemId||!F.name||typeof F.unitPrice!=="number")return null;return{...F,id:F.id,lineItemId:F.lineItemId,checkoutLinkKey:F.checkoutLinkKey??this.defaultCheckoutLinkKey,name:F.name,unitPrice:F.unitPrice,quantity:H(F.quantity)}}async post(F,G){let J=`${this.endpoint}${F}`,M=JSON.stringify(G),O={"Content-Type":"application/json"},X={body:G,bodyText:M,endpoint:this.endpoint,headers:O,path:F,siteKey:this.siteKey,url:J},z=typeof this.requestHeaders==="function"?await this.requestHeaders(X):this.requestHeaders,D=b(O,z),Y=await fetch(J,{method:"POST",headers:D,body:M}),N=await Y.json().catch(()=>null);if(!Y.ok){let U=typeof N==="object"&&N&&"code"in N?N.code:"request_failed",W=typeof N==="object"&&N&&"message"in N?N.message:"Cart request failed";throw new V(U??"request_failed",W??"Cart request failed",Y.status)}return N}commit(){if(this.persist&&this.storage)try{let F={version:P,items:this.items};this.storage.setItem(this.storageKey,JSON.stringify(F))}catch(F){this.log("Failed to persist cart",F)}this.emit()}emit(){let F=this.getState();for(let G of this.listeners)G(F)}log(...F){if(this.debug)console.log("[@behindthescenes/cart]",...F)}}function T(F){return Z.init(F)}export{T as createBTSCart,V as BTSCartError,Z as BTSCart};
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "@behindthescenes/cart",
3
+ "version": "0.0.3",
4
+ "generatedAt": "2026-05-14T11:25:11.511Z",
5
+ "artifacts": {
6
+ "browser": "browser/browser.js",
7
+ "cjs": "cjs/index.js",
8
+ "esm": "esm/index.js",
9
+ "types": "types/index.d.ts"
10
+ }
11
+ }
@@ -0,0 +1,3 @@
1
+ import { BTSCart, createBTSCart } from "./index";
2
+ export { BTSCart, createBTSCart };
3
+ export type { BTSCartCheckoutItemPayload, BTSCartCheckoutLinkConfig, BTSCartCheckoutUrlRequest, BTSCartCheckoutUrlResult, BTSCartInit, BTSCartItem, BTSCartItemInput, BTSCartState, } from "./types/index.types";
@@ -0,0 +1,4 @@
1
+ export declare const DEFAULT_ENDPOINT = "https://api.bts.it.com/v2/website/cart";
2
+ export declare const DEFAULT_STORAGE_KEY = "bts-cart";
3
+ export declare const DEFAULT_CHECKOUT_LINK_KEY = "default";
4
+ export declare const STORAGE_VERSION = 1;
@@ -0,0 +1,41 @@
1
+ import type { BTSCartInit, BTSCartItem, BTSCartItemInput, BTSCartListener, BTSCartState } from "./types/index.types";
2
+ export declare class BTSCart {
3
+ private siteKey;
4
+ private endpoint;
5
+ private debug;
6
+ private persist;
7
+ private storageKey;
8
+ private storage?;
9
+ private checkoutLinks;
10
+ private defaultCheckoutLinkKey;
11
+ private requestHeaders?;
12
+ private items;
13
+ private listeners;
14
+ constructor(init: BTSCartInit);
15
+ static init(opts: BTSCartInit): BTSCart;
16
+ getItems(): BTSCartItem[];
17
+ getState(): BTSCartState;
18
+ getItemCount(): number;
19
+ getSubtotal(): number;
20
+ getTotal(): number;
21
+ subscribe(listener: BTSCartListener): () => void;
22
+ destroy(): void;
23
+ rehydrate(): void;
24
+ addItem(input: BTSCartItemInput): void;
25
+ removeItem(id: string): void;
26
+ setQuantity(id: string, quantity: number): void;
27
+ increment(id: string, amount?: number): void;
28
+ decrement(id: string, amount?: number): void;
29
+ clear(): void;
30
+ resolveCheckoutUrl(checkoutLinkKey?: string): Promise<string>;
31
+ checkout(checkoutLinkKey?: string): Promise<string>;
32
+ private itemsForCheckout;
33
+ private normalizeItem;
34
+ private post;
35
+ private commit;
36
+ private emit;
37
+ private log;
38
+ }
39
+ export declare function createBTSCart(init: BTSCartInit): BTSCart;
40
+ export type { BTSCartCheckoutItemPayload, BTSCartCheckoutLinkConfig, BTSCartCheckoutUrlRequest, BTSCartCheckoutUrlResult, BTSCartErrorCode, BTSCartInit, BTSCartItem, BTSCartItemInput, BTSCartListener, BTSCartMode, BTSCartRequestContext, BTSCartRequestHeaders, BTSCartState, BTSCartStorage, } from "./types/index.types";
41
+ export { BTSCartError } from "./types/index.types";
@@ -0,0 +1,14 @@
1
+ import type { BTSCart, createBTSCart } from "../index";
2
+ declare global {
3
+ interface Window {
4
+ BTSCart?: {
5
+ BTSCart: typeof BTSCart;
6
+ createBTSCart: typeof createBTSCart;
7
+ };
8
+ createBTSCart?: typeof createBTSCart;
9
+ btsCart?: BTSCart;
10
+ btsCartDataLayer?: Array<ArrayLike<unknown>>;
11
+ btsCartCommand?: (...args: unknown[]) => void;
12
+ }
13
+ }
14
+ export {};
@@ -0,0 +1,75 @@
1
+ export type BTSCartMode = "static" | "dynamic";
2
+ export type BTSCartStorage = Pick<Storage, "getItem" | "setItem" | "removeItem">;
3
+ export type BTSCartRequestContext = {
4
+ body: unknown;
5
+ bodyText: string;
6
+ endpoint: string;
7
+ headers: Record<string, string>;
8
+ path: string;
9
+ siteKey: string;
10
+ url: string;
11
+ };
12
+ export type BTSCartRequestHeaders = HeadersInit | ((request: BTSCartRequestContext) => HeadersInit | Promise<HeadersInit>);
13
+ export type BTSCartCheckoutLinkConfig = {
14
+ /** Raw checkout link ID, or a full BTS checkout URL like `https://behindthescenes.com/c/<id>?discountCode=CODE`. */
15
+ checkoutLinkId: string;
16
+ mode: BTSCartMode;
17
+ discountCode?: string;
18
+ };
19
+ export type BTSCartItemInput = {
20
+ id: string;
21
+ lineItemId: string;
22
+ checkoutLinkKey?: string;
23
+ productId?: string;
24
+ accessTierId?: string;
25
+ name: string;
26
+ unitPrice: number;
27
+ fullPrice?: number;
28
+ image?: string;
29
+ quantity?: number;
30
+ /** Internal promo-code ID or customer-facing promo code. Customer codes are resolved by the BTS cart API. */
31
+ discount?: string;
32
+ };
33
+ export type BTSCartItem = Required<Pick<BTSCartItemInput, "id" | "lineItemId" | "name" | "unitPrice" | "quantity">> & Omit<BTSCartItemInput, "quantity"> & {
34
+ checkoutLinkKey: string;
35
+ };
36
+ export type BTSCartState = {
37
+ items: BTSCartItem[];
38
+ };
39
+ export type BTSCartListener = (state: BTSCartState) => void;
40
+ export type BTSCartInit = {
41
+ siteKey: string;
42
+ endpoint?: string;
43
+ checkoutLinks?: Record<string, BTSCartCheckoutLinkConfig>;
44
+ defaultCheckoutLinkKey?: string;
45
+ debug?: boolean;
46
+ persist?: boolean;
47
+ storageKey?: string;
48
+ storage?: BTSCartStorage;
49
+ requestHeaders?: BTSCartRequestHeaders;
50
+ };
51
+ export type BTSCartCheckoutItemPayload = {
52
+ lineItemId: string;
53
+ quantity: number;
54
+ discount?: string;
55
+ };
56
+ export type BTSCartCheckoutUrlRequest = {
57
+ siteKey: string;
58
+ checkoutLinkId: string;
59
+ mode: BTSCartMode;
60
+ items?: BTSCartCheckoutItemPayload[];
61
+ discountCode?: string;
62
+ };
63
+ export type BTSCartCheckoutUrlResult = {
64
+ ok: true;
65
+ checkoutUrl: string;
66
+ checkoutLinkId: string;
67
+ mode: BTSCartMode;
68
+ items: BTSCartCheckoutItemPayload[];
69
+ };
70
+ export type BTSCartErrorCode = "unknown_site" | "domain_not_verified" | "analytics_disabled" | "checkout_link_not_found" | "checkout_link_inactive" | "checkout_link_expired" | "checkout_link_sold_out" | "checkout_link_mode_mismatch" | "cart_empty" | "line_item_not_found" | "invalid_quantity" | "line_item_sold_out" | "product_unavailable" | "discount_invalid" | "request_failed";
71
+ export declare class BTSCartError extends Error {
72
+ code: BTSCartErrorCode;
73
+ status?: number;
74
+ constructor(code: BTSCartErrorCode, message: string, status?: number);
75
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@behindthescenes/cart",
3
+ "version": "0.0.3",
4
+ "description": "Browser cart SDK for BTS external-site checkout links.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "main": "./dist/cjs/index.js",
8
+ "module": "./dist/esm/index.js",
9
+ "types": "./dist/types/index.d.ts",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/gwop-company/bts-backend.git",
13
+ "directory": "packages/cart"
14
+ },
15
+ "keywords": [
16
+ "cart",
17
+ "checkout",
18
+ "browser",
19
+ "sdk"
20
+ ],
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/types/index.d.ts",
24
+ "import": "./dist/esm/index.js",
25
+ "require": "./dist/cjs/index.js",
26
+ "default": "./dist/esm/index.js"
27
+ },
28
+ "./browser": {
29
+ "types": "./dist/types/browser.d.ts",
30
+ "import": "./dist/browser/browser.js",
31
+ "default": "./dist/browser/browser.js"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "docs",
38
+ "README.md"
39
+ ],
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "scripts": {
44
+ "build": "bun ./scripts/build.ts",
45
+ "build:hosted": "bun ./scripts/sync-hosted-bundle.ts",
46
+ "build:hosted:force": "bun ./scripts/sync-hosted-bundle.ts --build",
47
+ "release:version": "bun ./scripts/bump-release-version.ts",
48
+ "type-check": "tsc --noEmit",
49
+ "test": "bun test",
50
+ "lint": "eslint . --fix",
51
+ "lint:check": "eslint .",
52
+ "release:check": "bun run lint:check && bun run type-check && bun run test && bun run build && npm pack --dry-run",
53
+ "release:publish": "bun run release:version && bun run release:check && npm publish --access public",
54
+ "prepublishOnly": "bun run release:check && bun ./scripts/sync-hosted-bundle.ts"
55
+ },
56
+ "devDependencies": {
57
+ "@bts/eslint-config": "workspace:*",
58
+ "@types/bun": "^1.3.11",
59
+ "eslint": "^9.17.0",
60
+ "typescript": "^5.8.3"
61
+ },
62
+ "peerDependencies": {
63
+ "typescript": "^5"
64
+ }
65
+ }