commerceforai 0.0.0-stage → 0.1.0
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 +39 -2
- package/dist/cart-core.d.ts +90 -0
- package/dist/cart-core.js +122 -0
- package/dist/checkout-handler.d.ts +26 -0
- package/dist/checkout-handler.js +63 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +216 -0
- package/dist/index.d.ts +761 -0
- package/dist/index.js +526 -0
- package/dist/react.d.ts +85 -0
- package/dist/react.js +162 -0
- package/dist/types.d.ts +530 -0
- package/dist/types.js +1 -0
- package/package.json +54 -3
- package/templates/nextjs/app/shop/[id]/page.tsx +44 -0
- package/templates/nextjs/app/shop/actions.ts +67 -0
- package/templates/nextjs/app/shop/cart/page.tsx +51 -0
- package/templates/nextjs/app/shop/cart-cookie.ts +22 -0
- package/templates/nextjs/app/shop/layout.tsx +25 -0
- package/templates/nextjs/app/shop/orders/lookup-form.tsx +32 -0
- package/templates/nextjs/app/shop/orders/page.tsx +13 -0
- package/templates/nextjs/app/shop/page.tsx +31 -0
- package/templates/nextjs/app/shop/shop.css +31 -0
- package/templates/nextjs/app/shop/thanks/clear-cart.tsx +10 -0
- package/templates/nextjs/app/shop/thanks/page.tsx +47 -0
- package/templates/nextjs/lib/commerce.ts +12 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
const env = (k) => typeof process !== "undefined" ? process.env?.[k] : undefined;
|
|
2
|
+
/**
|
|
3
|
+
* ```ts
|
|
4
|
+
* const commerce = new Commerce(process.env.COMMERCEFORAI_API_KEY);
|
|
5
|
+
* const { data, error } = await commerce.checkouts.create({ items: [{ variant_id, quantity: 1 }], success_url });
|
|
6
|
+
* ```
|
|
7
|
+
*/
|
|
8
|
+
export class Commerce {
|
|
9
|
+
products;
|
|
10
|
+
variants;
|
|
11
|
+
inventory;
|
|
12
|
+
checkouts;
|
|
13
|
+
orders;
|
|
14
|
+
events;
|
|
15
|
+
webhooks;
|
|
16
|
+
store;
|
|
17
|
+
returns;
|
|
18
|
+
emails;
|
|
19
|
+
collections;
|
|
20
|
+
customers;
|
|
21
|
+
discounts;
|
|
22
|
+
reports;
|
|
23
|
+
printify;
|
|
24
|
+
files;
|
|
25
|
+
key;
|
|
26
|
+
baseUrl;
|
|
27
|
+
maxRetries;
|
|
28
|
+
fetchImpl;
|
|
29
|
+
constructor(apiKey, opts = {}) {
|
|
30
|
+
const key = apiKey ?? env("COMMERCEFORAI_API_KEY");
|
|
31
|
+
if (!key)
|
|
32
|
+
throw new Error("Missing API key. Pass it to `new Commerce(key)` or set COMMERCEFORAI_API_KEY.");
|
|
33
|
+
this.key = key;
|
|
34
|
+
this.baseUrl = (opts.baseUrl ?? env("COMMERCEFORAI_BASE_URL") ?? "https://api.commerceforai.com").replace(/\/$/, "");
|
|
35
|
+
this.maxRetries = opts.maxRetries ?? 2;
|
|
36
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
37
|
+
this.products = new Products(this);
|
|
38
|
+
this.variants = new Variants(this);
|
|
39
|
+
this.inventory = new InventoryResource(this);
|
|
40
|
+
this.checkouts = new Checkouts(this);
|
|
41
|
+
this.orders = new Orders(this);
|
|
42
|
+
this.events = new Events(this);
|
|
43
|
+
this.webhooks = new Webhooks(this);
|
|
44
|
+
this.store = new StoreResource(this);
|
|
45
|
+
this.returns = new Returns(this);
|
|
46
|
+
this.emails = new Emails(this);
|
|
47
|
+
this.collections = new Collections(this);
|
|
48
|
+
this.customers = new Customers(this);
|
|
49
|
+
this.discounts = new Discounts(this);
|
|
50
|
+
this.reports = new Reports(this);
|
|
51
|
+
this.printify = new PrintifyResource(this);
|
|
52
|
+
this.files = new Files(this);
|
|
53
|
+
}
|
|
54
|
+
/** @internal */
|
|
55
|
+
async request(method, path, opts = {}) {
|
|
56
|
+
const url = new URL(this.baseUrl + path);
|
|
57
|
+
for (const [k, v] of Object.entries(opts.query ?? {}))
|
|
58
|
+
if (v !== undefined)
|
|
59
|
+
url.searchParams.set(k, String(v));
|
|
60
|
+
const headers = {
|
|
61
|
+
authorization: `Bearer ${this.key}`,
|
|
62
|
+
"user-agent": "commerceforai-node/0.1.0",
|
|
63
|
+
};
|
|
64
|
+
if (opts.body !== undefined)
|
|
65
|
+
headers["content-type"] = "application/json";
|
|
66
|
+
if (method === "POST")
|
|
67
|
+
headers["idempotency-key"] = opts.idempotencyKey ?? crypto.randomUUID();
|
|
68
|
+
let lastError = { type: "network_error", message: "Request failed" };
|
|
69
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
70
|
+
if (attempt > 0)
|
|
71
|
+
await new Promise((r) => setTimeout(r, 250 * 2 ** (attempt - 1)));
|
|
72
|
+
let res;
|
|
73
|
+
try {
|
|
74
|
+
res = await this.fetchImpl(url, {
|
|
75
|
+
method, headers, body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
lastError = { type: "network_error", message: e instanceof Error ? e.message : String(e) };
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const json = await res.json().catch(() => null);
|
|
83
|
+
if (res.ok)
|
|
84
|
+
return { data: json, error: null };
|
|
85
|
+
lastError = json?.error ?? { type: "internal_error", message: `HTTP ${res.status}` };
|
|
86
|
+
if (res.status !== 429 && res.status < 500)
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
return { data: null, error: lastError };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
class Resource {
|
|
93
|
+
client;
|
|
94
|
+
constructor(client) {
|
|
95
|
+
this.client = client;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
class Products extends Resource {
|
|
99
|
+
/**
|
|
100
|
+
* Create up to 100 products in one request. Each succeeds or fails on its own; retry only the failures.
|
|
101
|
+
* Returns { created, failed, results: [{ index, product } | { index, error }] }.
|
|
102
|
+
*/
|
|
103
|
+
createMany(products) {
|
|
104
|
+
return this.client.request("POST", "/v1/products/bulk", { body: { products } });
|
|
105
|
+
}
|
|
106
|
+
/** Create a product. Use `price` for a single-variant product, or `variants` for sizes/colors. */
|
|
107
|
+
create(params, opts) {
|
|
108
|
+
return this.client.request("POST", "/v1/products", { body: params, ...opts });
|
|
109
|
+
}
|
|
110
|
+
get(id) {
|
|
111
|
+
return this.client.request("GET", `/v1/products/${encodeURIComponent(id)}`);
|
|
112
|
+
}
|
|
113
|
+
/** `metadata` filters on exact matches, e.g. { collection: "yosemite" }. */
|
|
114
|
+
/** `q` searches product names and SKUs. */
|
|
115
|
+
list(params = {}) {
|
|
116
|
+
const { metadata, ...rest } = params;
|
|
117
|
+
const query = { ...rest };
|
|
118
|
+
for (const [k, v] of Object.entries(metadata ?? {}))
|
|
119
|
+
query[`metadata[${k}]`] = v;
|
|
120
|
+
return this.client.request("GET", "/v1/products", { query });
|
|
121
|
+
}
|
|
122
|
+
update(id, params) {
|
|
123
|
+
return this.client.request("PATCH", `/v1/products/${encodeURIComponent(id)}`, { body: params });
|
|
124
|
+
}
|
|
125
|
+
/** Archive: hides it from sale; existing orders are unaffected. */
|
|
126
|
+
archive(id) {
|
|
127
|
+
return this.client.request("DELETE", `/v1/products/${encodeURIComponent(id)}`);
|
|
128
|
+
}
|
|
129
|
+
addVariant(productId, params) {
|
|
130
|
+
return this.client.request("POST", `/v1/products/${encodeURIComponent(productId)}/variants`, { body: params });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
class Variants extends Resource {
|
|
134
|
+
update(id, params) {
|
|
135
|
+
return this.client.request("PATCH", `/v1/variants/${encodeURIComponent(id)}`, { body: params });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
class InventoryResource extends Resource {
|
|
139
|
+
/** Tracked variants, lowest stock first. */
|
|
140
|
+
list(params = {}) {
|
|
141
|
+
return this.client.request("GET", "/v1/inventory", { query: params });
|
|
142
|
+
}
|
|
143
|
+
/** Add/remove units, e.g. `{ adjust: 50 }` after a restock or `{ adjust: -1 }` for damage. */
|
|
144
|
+
adjust(variantId, adjust, reason) {
|
|
145
|
+
return this.client.request("POST", `/v1/variants/${encodeURIComponent(variantId)}/inventory`, { body: { adjust, reason } });
|
|
146
|
+
}
|
|
147
|
+
/** Set the absolute on-hand count, e.g. after a stock count. */
|
|
148
|
+
set(variantId, onHand, reason) {
|
|
149
|
+
return this.client.request("POST", `/v1/variants/${encodeURIComponent(variantId)}/inventory`, { body: { set: onHand, reason } });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
class Checkouts extends Resource {
|
|
153
|
+
/** Reserve stock and create a Stripe-hosted checkout. Redirect the customer to `data.url`. */
|
|
154
|
+
create(params, opts) {
|
|
155
|
+
return this.client.request("POST", "/v1/checkouts", { body: params, ...opts });
|
|
156
|
+
}
|
|
157
|
+
get(id) {
|
|
158
|
+
return this.client.request("GET", `/v1/checkouts/${encodeURIComponent(id)}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
class Orders extends Resource {
|
|
162
|
+
/** `q` matches an order number, customer email or name. */
|
|
163
|
+
list(params = {}) {
|
|
164
|
+
const status = Array.isArray(params.status) ? params.status.join(",") : params.status;
|
|
165
|
+
return this.client.request("GET", "/v1/orders", { query: { ...params, status } });
|
|
166
|
+
}
|
|
167
|
+
/** Accepts an order id (`ord_...`) or order number (`1001`). */
|
|
168
|
+
get(idOrNumber) {
|
|
169
|
+
return this.client.request("GET", `/v1/orders/${encodeURIComponent(String(idOrNumber))}`);
|
|
170
|
+
}
|
|
171
|
+
update(id, params) {
|
|
172
|
+
return this.client.request("PATCH", `/v1/orders/${encodeURIComponent(String(id))}`, { body: params });
|
|
173
|
+
}
|
|
174
|
+
/** Mark shipped. Calling again updates tracking. */
|
|
175
|
+
fulfill(id, params = {}) {
|
|
176
|
+
return this.client.request("POST", `/v1/orders/${encodeURIComponent(String(id))}/fulfill`, { body: params });
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get live label rates from your EasyPost/Shippo account, cheapest first.
|
|
180
|
+
* Parcel weight defaults to the sum of variant `weight_oz`. Pass `return_id` for return-label rates.
|
|
181
|
+
*/
|
|
182
|
+
shippingRates(id, params = {}) {
|
|
183
|
+
return this.client.request("POST", `/v1/orders/${encodeURIComponent(String(id))}/shipping-rates`, { body: params });
|
|
184
|
+
}
|
|
185
|
+
/** Buy a label for a rate. Outbound labels also mark the order fulfilled with tracking (and email the customer). */
|
|
186
|
+
buyLabel(id, rateId, opts = {}) {
|
|
187
|
+
return this.client.request("POST", `/v1/orders/${encodeURIComponent(String(id))}/labels`, { body: { rate_id: rateId, ...opts } });
|
|
188
|
+
}
|
|
189
|
+
/** Send a paid order to the print-on-demand provider now (e.g. after fixing a missing print file). */
|
|
190
|
+
submitFulfillment(id, params = {}) {
|
|
191
|
+
return this.client.request("POST", `/v1/orders/${encodeURIComponent(String(id))}/submit-fulfillment`, { body: params });
|
|
192
|
+
}
|
|
193
|
+
labels(id) {
|
|
194
|
+
return this.client.request("GET", `/v1/orders/${encodeURIComponent(String(id))}/labels`);
|
|
195
|
+
}
|
|
196
|
+
/** Refund through Stripe. Omit `amount` for a full refund; `restock` returns items to inventory. */
|
|
197
|
+
refund(id, params = {}) {
|
|
198
|
+
return this.client.request("POST", `/v1/orders/${encodeURIComponent(String(id))}/refund`, { body: params });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
class Collections extends Resource {
|
|
202
|
+
/** Group products, e.g. { name: "Mugs", product_ids: [...] }. The handle defaults from the name. */
|
|
203
|
+
create(params) {
|
|
204
|
+
return this.client.request("POST", "/v1/collections", { body: params });
|
|
205
|
+
}
|
|
206
|
+
list() { return this.client.request("GET", "/v1/collections"); }
|
|
207
|
+
/** By id or handle. */
|
|
208
|
+
get(idOrHandle) { return this.client.request("GET", `/v1/collections/${encodeURIComponent(idOrHandle)}`); }
|
|
209
|
+
/** Passing product_ids replaces the membership (in that order). */
|
|
210
|
+
update(idOrHandle, params) {
|
|
211
|
+
return this.client.request("PATCH", `/v1/collections/${encodeURIComponent(idOrHandle)}`, { body: params });
|
|
212
|
+
}
|
|
213
|
+
addProducts(idOrHandle, productIds) {
|
|
214
|
+
return this.client.request("POST", `/v1/collections/${encodeURIComponent(idOrHandle)}/products`, { body: { product_ids: productIds } });
|
|
215
|
+
}
|
|
216
|
+
removeProduct(idOrHandle, productId) {
|
|
217
|
+
return this.client.request("DELETE", `/v1/collections/${encodeURIComponent(idOrHandle)}/products/${encodeURIComponent(productId)}`);
|
|
218
|
+
}
|
|
219
|
+
delete(idOrHandle) {
|
|
220
|
+
return this.client.request("DELETE", `/v1/collections/${encodeURIComponent(idOrHandle)}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
class Customers extends Resource {
|
|
224
|
+
/** Everyone who has paid, matched by email. Search by email or name; sort by recent (default), spent or orders. */
|
|
225
|
+
list(params = {}) {
|
|
226
|
+
return this.client.request("GET", "/v1/customers", { query: params });
|
|
227
|
+
}
|
|
228
|
+
/** Totals, last address and full order history for one email. */
|
|
229
|
+
get(email) { return this.client.request("GET", `/v1/customers/${encodeURIComponent(email)}`); }
|
|
230
|
+
}
|
|
231
|
+
class Discounts extends Resource {
|
|
232
|
+
/** e.g. { code: "SUMMER20", kind: "percent", value: 20 } or { code: "FIVEOFF", kind: "amount", value: 500 }. */
|
|
233
|
+
create(params) { return this.client.request("POST", "/v1/discounts", { body: params }); }
|
|
234
|
+
list() { return this.client.request("GET", "/v1/discounts"); }
|
|
235
|
+
/** By id or code. */
|
|
236
|
+
get(idOrCode) { return this.client.request("GET", `/v1/discounts/${encodeURIComponent(idOrCode)}`); }
|
|
237
|
+
update(idOrCode, params) {
|
|
238
|
+
return this.client.request("PATCH", `/v1/discounts/${encodeURIComponent(idOrCode)}`, { body: params });
|
|
239
|
+
}
|
|
240
|
+
delete(idOrCode) { return this.client.request("DELETE", `/v1/discounts/${encodeURIComponent(idOrCode)}`); }
|
|
241
|
+
}
|
|
242
|
+
class Reports extends Resource {
|
|
243
|
+
/** Sales over the last `days` (default 30, max 366): totals vs the previous period, a time series and top products. */
|
|
244
|
+
sales(params = {}) { return this.client.request("GET", "/v1/reports/sales", { query: params }); }
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Printify, through the shop the store connected (your agent never needs the Printify token).
|
|
248
|
+
* Find a blueprint → a print provider → its variants, then create the product in Printify.
|
|
249
|
+
*/
|
|
250
|
+
class PrintifyResource extends Resource {
|
|
251
|
+
/** Search Printify's catalog, e.g. blueprints({ q: "heavy cotton tee" }). */
|
|
252
|
+
blueprints(params = {}) {
|
|
253
|
+
return this.client.request("GET", "/v1/fulfillment/printify/blueprints", { query: params });
|
|
254
|
+
}
|
|
255
|
+
providers(blueprintId) {
|
|
256
|
+
return this.client.request("GET", `/v1/fulfillment/printify/blueprints/${blueprintId}/providers`);
|
|
257
|
+
}
|
|
258
|
+
/** Variant ids per size/color, the option values, and the print placements ("front", "back"…). */
|
|
259
|
+
variants(blueprintId, providerId) {
|
|
260
|
+
return this.client.request("GET", `/v1/fulfillment/printify/blueprints/${blueprintId}/providers/${providerId}/variants`);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Create a CommerceForAI product in your Printify shop (artwork uploaded, variants priced, mockups made) and map
|
|
264
|
+
* its variants so paid orders go to production. Variants are matched by option values; pass fixed_options for
|
|
265
|
+
* options yours don't have (e.g. { color: "Black" }), or variant_map to map explicitly.
|
|
266
|
+
*/
|
|
267
|
+
createProduct(productId, params) {
|
|
268
|
+
return this.client.request("POST", `/v1/products/${encodeURIComponent(productId)}/printify`, { body: params });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Image hosting (product photos, logos, print files): PNG, JPEG or WebP up to 25 MB, 5 GB per store.
|
|
273
|
+
* Photo metadata is removed; the same image is stored once; URLs never change.
|
|
274
|
+
*/
|
|
275
|
+
class Files extends Resource {
|
|
276
|
+
/**
|
|
277
|
+
* Upload bytes (Blob, File, Buffer, Uint8Array) or copy from a link.
|
|
278
|
+
* ```ts
|
|
279
|
+
* await commerce.files.upload(readFileSync("art/half-dome.png"), { filename: "half-dome.png" });
|
|
280
|
+
* await commerce.files.upload({ url: "https://example.com/photo.jpg" });
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
upload(input, opts = {}) {
|
|
284
|
+
if (input && typeof input === "object" && "url" in input && typeof input.url === "string")
|
|
285
|
+
return this.client.request("POST", "/v1/files", { body: { url: input.url } });
|
|
286
|
+
const bytes = input instanceof Blob ? input : new Blob([input]);
|
|
287
|
+
return bytes.arrayBuffer().then((buf) => this.client.request("POST", "/v1/files", {
|
|
288
|
+
body: { data: base64(new Uint8Array(buf)), filename: opts.filename ?? (input instanceof File ? input.name : undefined) },
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
list(params = {}) {
|
|
292
|
+
return this.client.request("GET", "/v1/files", { query: params });
|
|
293
|
+
}
|
|
294
|
+
delete(id) { return this.client.request("DELETE", `/v1/files/${encodeURIComponent(id)}`); }
|
|
295
|
+
/**
|
|
296
|
+
* Copy products' outside images (and print files) onto the CDN so a moved or deleted source can't break the store.
|
|
297
|
+
*/
|
|
298
|
+
copyProductImages(productId) {
|
|
299
|
+
return this.client.request("POST", `/v1/products/${encodeURIComponent(productId)}/copy-images`);
|
|
300
|
+
}
|
|
301
|
+
/** The same for every product with outside images, up to `limit` (≤50) per call. Repeat until `remaining` is 0. */
|
|
302
|
+
copyAllProductImages(opts = {}) {
|
|
303
|
+
return this.client.request("POST", "/v1/products/copy-images", { body: opts });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function base64(bytes) {
|
|
307
|
+
if (typeof Buffer !== "undefined")
|
|
308
|
+
return Buffer.from(bytes).toString("base64");
|
|
309
|
+
let s = "";
|
|
310
|
+
for (let i = 0; i < bytes.length; i += 0x8000)
|
|
311
|
+
s += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
312
|
+
return btoa(s);
|
|
313
|
+
}
|
|
314
|
+
class Returns extends Resource {
|
|
315
|
+
/** Open a return for items on a shipped order. */
|
|
316
|
+
create(orderId, params) {
|
|
317
|
+
return this.client.request("POST", `/v1/orders/${encodeURIComponent(String(orderId))}/returns`, { body: params });
|
|
318
|
+
}
|
|
319
|
+
list(params = {}) {
|
|
320
|
+
return this.client.request("GET", "/v1/returns", { query: { ...params, order_id: params.order_id?.toString() } });
|
|
321
|
+
}
|
|
322
|
+
get(id) {
|
|
323
|
+
return this.client.request("GET", `/v1/returns/${encodeURIComponent(id)}`);
|
|
324
|
+
}
|
|
325
|
+
/** Approve. `return_label: "cheapest"` buys a prepaid return label and includes it in the customer email. */
|
|
326
|
+
approve(id, params = {}) {
|
|
327
|
+
return this.client.request("POST", `/v1/returns/${encodeURIComponent(id)}/approve`, { body: params });
|
|
328
|
+
}
|
|
329
|
+
reject(id, params = {}) {
|
|
330
|
+
return this.client.request("POST", `/v1/returns/${encodeURIComponent(id)}/reject`, { body: params });
|
|
331
|
+
}
|
|
332
|
+
/** Items arrived: restock (default true) and refund their value (default true; shipping not included). */
|
|
333
|
+
receive(id, params = {}) {
|
|
334
|
+
return this.client.request("POST", `/v1/returns/${encodeURIComponent(id)}/receive`, { body: params });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
class Emails extends Resource {
|
|
338
|
+
/** Log of customer emails sent (or queued) for your store. */
|
|
339
|
+
list(params = {}) {
|
|
340
|
+
return this.client.request("GET", "/v1/emails", { query: { order_id: params.order_id?.toString() } });
|
|
341
|
+
}
|
|
342
|
+
/** Render a customer email with your branding and sample data. Templates: order_confirmation, order_shipped, order_refunded, return_approved, checkout_recovery. */
|
|
343
|
+
preview(template) {
|
|
344
|
+
return this.client.request("GET", `/v1/emails/preview/${template}`);
|
|
345
|
+
}
|
|
346
|
+
/** Send previews to the store owner (or the signed-in member): one template, or "all". */
|
|
347
|
+
sendTest(params = {}) {
|
|
348
|
+
return this.client.request("POST", "/v1/emails/test", { body: params });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
class Events extends Resource {
|
|
352
|
+
/** Poll events in order. Pass the last id you processed as `after`. `type` accepts wildcards like `order.*`. */
|
|
353
|
+
list(params = {}) {
|
|
354
|
+
return this.client.request("GET", "/v1/events", { query: params });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
class StoreResource extends Resource {
|
|
358
|
+
get() {
|
|
359
|
+
return this.client.request("GET", "/v1/store");
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Note on `tax_enabled`: Stripe only collects tax in jurisdictions where you have an active
|
|
363
|
+
* tax registration in Stripe Tax. Turning this on without registrations collects $0 tax.
|
|
364
|
+
*/
|
|
365
|
+
/**
|
|
366
|
+
* Preview a sync from this store's other version (test or live) into this store. Nothing changes.
|
|
367
|
+
* Syncs cover products, prices, images, collections, discounts and shipping/email settings; never orders, keys, Stripe, site URLs or stock counts.
|
|
368
|
+
*/
|
|
369
|
+
syncPreview() { return this.client.request("GET", "/v1/store/sync"); }
|
|
370
|
+
/** Apply a sync into this store. Into a live store, pass the `plan_id` from syncPreview(). */
|
|
371
|
+
sync(params = {}) {
|
|
372
|
+
return this.client.request("POST", "/v1/store/sync", { body: params });
|
|
373
|
+
}
|
|
374
|
+
update(params) {
|
|
375
|
+
return this.client.request("PATCH", "/v1/store", { body: params });
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Would OpenAI's ChatGPT shopping accept this store's product feed? Per-item errors (rejected) and
|
|
379
|
+
* warnings (weaker listings), plus where the feed lives (JSON and the jsonl.gz upload format).
|
|
380
|
+
*/
|
|
381
|
+
checkFeed() {
|
|
382
|
+
return this.client.request("GET", "/v1/feeds/openai/check");
|
|
383
|
+
}
|
|
384
|
+
/** Sell in ChatGPT: readiness steps, suggested answers for OpenAI's merchant application, and status. */
|
|
385
|
+
chatgpt() {
|
|
386
|
+
return this.client.request("GET", "/v1/channels/chatgpt");
|
|
387
|
+
}
|
|
388
|
+
/** Record the application's progress (after the owner submits it) or the product category to apply with. */
|
|
389
|
+
updateChatgpt(params) {
|
|
390
|
+
return this.client.request("PATCH", "/v1/channels/chatgpt", { body: params });
|
|
391
|
+
}
|
|
392
|
+
/** Everything between this store and real orders, each with the exact fix. Start here when setting up. */
|
|
393
|
+
setupStatus() {
|
|
394
|
+
return this.client.request("GET", "/v1/setup_status");
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Test stores only: run a purchase end to end without a browser. Stock is reserved and committed, the
|
|
398
|
+
* order is created and paid, events and your webhooks fire; `fulfill: true` also marks it shipped.
|
|
399
|
+
* Never charges a card, reaches Printify/Printful, emails anyone, or counts toward usage.
|
|
400
|
+
*/
|
|
401
|
+
simulateOrder(params) {
|
|
402
|
+
return this.client.request("POST", "/v1/simulate/order", { body: params });
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* A one-time link (valid 7 days) to hand to a person when the store is ready for real orders. Opening it
|
|
406
|
+
* signs them in, claims the store if it's an unclaimed sandbox, creates a live copy (products, prices,
|
|
407
|
+
* inventory, settings and connections), and takes them to Connect Stripe. Test stores only.
|
|
408
|
+
*/
|
|
409
|
+
goLiveLink() {
|
|
410
|
+
return this.client.request("POST", "/v1/go_live_links", { body: {} });
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Shipping every checkout offers unless it passes its own rates: up to 5 flat rates (cents), the
|
|
414
|
+
* countries you ship to, and an optional free-shipping threshold on the item subtotal (cents).
|
|
415
|
+
*/
|
|
416
|
+
configureCheckoutShipping(params) {
|
|
417
|
+
return this.client.request("PUT", "/v1/store/checkout-shipping", { body: params });
|
|
418
|
+
}
|
|
419
|
+
/** Connect your EasyPost or Shippo account for label purchasing. */
|
|
420
|
+
configureShipping(params) {
|
|
421
|
+
return this.client.request("PUT", "/v1/store/shipping", { body: params });
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Connect Printful or Printify (call once per provider; both can be connected — each variant routes to the
|
|
425
|
+
* provider it's mapped to). Paid orders with mapped variants are sent automatically — held for your
|
|
426
|
+
* approval in the provider's dashboard unless `auto_confirm: true`. Shipments mark orders fulfilled.
|
|
427
|
+
*/
|
|
428
|
+
configureFulfillment(params) {
|
|
429
|
+
return this.client.request("PUT", "/v1/store/fulfillment", { body: params });
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Import products from Shopify: pass Shopify's product export CSV (Products → Export) as `csv`, or
|
|
433
|
+
* `shop` (your-store.myshopify.com) + a custom app `access_token` (read_products, read_inventory).
|
|
434
|
+
* Safe to re-run: products match by Shopify id or SKU and existing prices and stock are kept.
|
|
435
|
+
*/
|
|
436
|
+
importFromShopify(params) {
|
|
437
|
+
return this.client.request("POST", "/v1/import/shopify", { body: params });
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Import the products already set up in a connected Printify shop or Printful store (both when omitted).
|
|
441
|
+
* Runs in the background and happens automatically on connect; safe to re-run. Progress: `store.get()` → fulfillment[].catalog_import.
|
|
442
|
+
*/
|
|
443
|
+
importCatalog(params = {}) {
|
|
444
|
+
return this.client.request("POST", "/v1/catalog/import", { body: params });
|
|
445
|
+
}
|
|
446
|
+
/** Disconnect one provider, or all when omitted. */
|
|
447
|
+
disableFulfillment(provider) {
|
|
448
|
+
return this.client.request("DELETE", "/v1/store/fulfillment", { query: { provider } });
|
|
449
|
+
}
|
|
450
|
+
/** Send customer emails (confirmation, shipped, refunded, return approved) from your domain via Resend. */
|
|
451
|
+
configureEmails(params) {
|
|
452
|
+
return this.client.request("PUT", "/v1/store/emails", { body: params });
|
|
453
|
+
}
|
|
454
|
+
disableEmails() {
|
|
455
|
+
return this.client.request("DELETE", "/v1/store/emails");
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Publish product feeds for AI shopping surfaces (OpenAI/ACP JSON, Google Merchant XML) and,
|
|
459
|
+
* with `agent_access: true`, a public API third-party shopping agents can use to create checkout links.
|
|
460
|
+
*/
|
|
461
|
+
configureStorefront(params) {
|
|
462
|
+
return this.client.request("PUT", "/v1/store/storefront", { body: params });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
class Webhooks extends Resource {
|
|
466
|
+
create(params) {
|
|
467
|
+
return this.client.request("POST", "/v1/webhook-endpoints", { body: params });
|
|
468
|
+
}
|
|
469
|
+
list() {
|
|
470
|
+
return this.client.request("GET", "/v1/webhook-endpoints");
|
|
471
|
+
}
|
|
472
|
+
remove(id) {
|
|
473
|
+
return this.client.request("DELETE", `/v1/webhook-endpoints/${encodeURIComponent(id)}`);
|
|
474
|
+
}
|
|
475
|
+
/** Verify a webhook we sent you. Throws if the signature is invalid or too old. */
|
|
476
|
+
verify(args) {
|
|
477
|
+
return verifyWebhook(args);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Verify a CommerceForAI webhook (Standard Webhooks format). Works in Node 18+, Bun, Deno and edge runtimes.
|
|
482
|
+
* Pass the raw request body string, not parsed JSON.
|
|
483
|
+
*/
|
|
484
|
+
export async function verifyWebhook({ payload, headers, secret, toleranceSeconds = 300, }) {
|
|
485
|
+
const get = (k) => headers instanceof Headers ? headers.get(k) ?? undefined : headers[k] ?? headers[k.toLowerCase()];
|
|
486
|
+
const msgId = get("webhook-id");
|
|
487
|
+
const ts = get("webhook-timestamp");
|
|
488
|
+
const sigHeader = get("webhook-signature");
|
|
489
|
+
if (!msgId || !ts || !sigHeader)
|
|
490
|
+
throw new Error("Missing webhook-id, webhook-timestamp or webhook-signature header");
|
|
491
|
+
const now = Math.floor(Date.now() / 1000);
|
|
492
|
+
if (!/^\d+$/.test(ts) || Math.abs(now - Number(ts)) > toleranceSeconds)
|
|
493
|
+
throw new Error("Webhook timestamp outside tolerance");
|
|
494
|
+
const keyBytes = base64ToBytes(secret.replace(/^whsec_/, ""));
|
|
495
|
+
const key = await crypto.subtle.importKey("raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
496
|
+
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${msgId}.${ts}.${payload}`)));
|
|
497
|
+
const expected = bytesToBase64(mac);
|
|
498
|
+
const ok = sigHeader.split(" ").some((part) => {
|
|
499
|
+
const [ver, sig] = part.split(",");
|
|
500
|
+
return ver === "v1" && sig !== undefined && timingSafeEqual(sig, expected);
|
|
501
|
+
});
|
|
502
|
+
if (!ok)
|
|
503
|
+
throw new Error("Invalid webhook signature");
|
|
504
|
+
return JSON.parse(payload);
|
|
505
|
+
}
|
|
506
|
+
function timingSafeEqual(a, b) {
|
|
507
|
+
if (a.length !== b.length)
|
|
508
|
+
return false;
|
|
509
|
+
let diff = 0;
|
|
510
|
+
for (let i = 0; i < a.length; i++)
|
|
511
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
512
|
+
return diff === 0;
|
|
513
|
+
}
|
|
514
|
+
function base64ToBytes(b64) {
|
|
515
|
+
const bin = atob(b64);
|
|
516
|
+
return Uint8Array.from(bin, (ch) => ch.charCodeAt(0));
|
|
517
|
+
}
|
|
518
|
+
function bytesToBase64(bytes) {
|
|
519
|
+
let bin = "";
|
|
520
|
+
for (const b of bytes)
|
|
521
|
+
bin += String.fromCharCode(b);
|
|
522
|
+
return btoa(bin);
|
|
523
|
+
}
|
|
524
|
+
export default Commerce;
|
|
525
|
+
export { createCheckoutHandler } from "./checkout-handler.js";
|
|
526
|
+
export * from "./cart-core.js";
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type CartLine, type CatalogProduct } from "./cart-core.js";
|
|
3
|
+
export * from "./cart-core.js";
|
|
4
|
+
type Listener = () => void;
|
|
5
|
+
/** A tiny external store: one per storage key, shared by every hook on the page. */
|
|
6
|
+
declare function createCartStore(key: string): {
|
|
7
|
+
get: () => CartLine[];
|
|
8
|
+
set(next: CartLine[]): void;
|
|
9
|
+
subscribe(l: Listener): () => void;
|
|
10
|
+
};
|
|
11
|
+
type CartStore = ReturnType<typeof createCartStore>;
|
|
12
|
+
export interface CartProviderProps {
|
|
13
|
+
children?: React.ReactNode;
|
|
14
|
+
/** localStorage key (default "cfa_cart"). Use one per store if a site sells for several. */
|
|
15
|
+
storageKey?: string;
|
|
16
|
+
/** Your server route that creates the checkout (default "/api/checkout"). */
|
|
17
|
+
checkoutEndpoint?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function CartProvider({ children, storageKey, checkoutEndpoint }: CartProviderProps): React.FunctionComponentElement<React.ProviderProps<{
|
|
20
|
+
store: CartStore;
|
|
21
|
+
endpoint: string;
|
|
22
|
+
} | null>>;
|
|
23
|
+
export type CheckoutResult = {
|
|
24
|
+
ok: true;
|
|
25
|
+
url: string;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
error: {
|
|
29
|
+
type: string;
|
|
30
|
+
message: string;
|
|
31
|
+
variant_id?: string;
|
|
32
|
+
available?: number;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The cart. Pass the catalog (`products`) to get names, current prices, a subtotal and stock checks;
|
|
37
|
+
* without it you still get lines and a count.
|
|
38
|
+
*/
|
|
39
|
+
export declare function useCart(products?: CatalogProduct[]): {
|
|
40
|
+
/** Raw lines (ids + quantities). */
|
|
41
|
+
items: CartLine[];
|
|
42
|
+
/** Lines joined to the catalog (when `products` is passed). */
|
|
43
|
+
lines: import("./cart-core.js").ResolvedLine[];
|
|
44
|
+
missing: CartLine[];
|
|
45
|
+
count: number;
|
|
46
|
+
subtotal: number;
|
|
47
|
+
currency: string;
|
|
48
|
+
ready: boolean;
|
|
49
|
+
add: (variantId: string, quantity?: number, properties?: Record<string, string>) => void;
|
|
50
|
+
setQuantity: (index: number, quantity: number) => void;
|
|
51
|
+
remove: (index: number) => void;
|
|
52
|
+
clear: () => void;
|
|
53
|
+
fix: () => void;
|
|
54
|
+
checkout: (opts?: {
|
|
55
|
+
discount_code?: string;
|
|
56
|
+
redirect?: boolean;
|
|
57
|
+
}) => Promise<CheckoutResult>;
|
|
58
|
+
/** True while creating the checkout / redirecting. */
|
|
59
|
+
pending: boolean;
|
|
60
|
+
/** The last checkout error (e.g. out_of_stock with variant_id and available). */
|
|
61
|
+
error: {
|
|
62
|
+
type: string;
|
|
63
|
+
message: string;
|
|
64
|
+
variant_id?: string;
|
|
65
|
+
available?: number;
|
|
66
|
+
} | null;
|
|
67
|
+
};
|
|
68
|
+
/** Selected options → variant, with which choices are still buyable. */
|
|
69
|
+
export declare function useVariantPicker(product: CatalogProduct, initial?: Record<string, string>): {
|
|
70
|
+
selected: Record<string, string>;
|
|
71
|
+
select: (name: string, value: string) => void;
|
|
72
|
+
options: {
|
|
73
|
+
name: string;
|
|
74
|
+
values: import("./cart-core.js").OptionChoice[];
|
|
75
|
+
}[];
|
|
76
|
+
variant: import("./cart-core.js").CatalogVariant | null;
|
|
77
|
+
inStock: boolean;
|
|
78
|
+
available: number | null;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Empty the cart after a successful payment. Call on your success page; it clears only when the URL
|
|
82
|
+
* has the `cfa_checkout` parameter that `createCheckoutHandler` adds to the success URL.
|
|
83
|
+
*/
|
|
84
|
+
export declare function useClearCartOnSuccess(param?: string): void;
|
|
85
|
+
export declare const formatMoney: (cents: number, currency?: string, locale?: string) => string;
|