epicmerch-mcp 1.3.0 → 1.3.2
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/package.json +1 -1
- package/skills/epicmerch-orders.md +54 -17
- package/skills/epicmerch-products.md +40 -4
- package/skills/epicmerch-storefront.md +112 -26
- package/src/index.js +9 -1
- package/src/install.js +20 -5
- package/src/tools/scaffold.js +372 -285
package/package.json
CHANGED
|
@@ -33,27 +33,46 @@ import { useEffect, useState } from 'react';
|
|
|
33
33
|
import { store } from '../lib/epicmerch';
|
|
34
34
|
|
|
35
35
|
export default function Cart({ onCheckout }) {
|
|
36
|
-
|
|
36
|
+
// store.cart.get() returns { cart, cartCount, cartTotal } — the cart line
|
|
37
|
+
// items are on `cart.cart`, NOT `cart.items`. Each line item is shaped
|
|
38
|
+
// { product: { _id, name, price, salePrice, images, ... }, qty, variant }.
|
|
39
|
+
const [cart, setCart] = useState({ cart: [] });
|
|
37
40
|
|
|
38
41
|
useEffect(() => { store.cart.get().then(setCart); }, []);
|
|
39
42
|
|
|
40
|
-
const remove = async (productId) => {
|
|
41
|
-
await store.cart.remove(productId);
|
|
42
|
-
// Optimistic local update —
|
|
43
|
-
|
|
43
|
+
const remove = async (productId, variant) => {
|
|
44
|
+
await store.cart.remove(productId, variant);
|
|
45
|
+
// Optimistic local update — match on BOTH productId AND variant so we
|
|
46
|
+
// don't accidentally remove a different size of the same product.
|
|
47
|
+
setCart(c => ({
|
|
48
|
+
...c,
|
|
49
|
+
cart: (c.cart || []).filter(
|
|
50
|
+
i => !(i.product._id === productId && i.variant === variant),
|
|
51
|
+
),
|
|
52
|
+
}));
|
|
44
53
|
};
|
|
45
54
|
|
|
46
|
-
const
|
|
55
|
+
const items = cart.cart || [];
|
|
56
|
+
const total = items.reduce((sum, i) => {
|
|
57
|
+
const price = i.product.salePrice ?? i.product.price;
|
|
58
|
+
return sum + price * i.qty;
|
|
59
|
+
}, 0);
|
|
47
60
|
|
|
48
61
|
return (
|
|
49
62
|
<div>
|
|
50
|
-
{
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
{items.map((item, idx) => {
|
|
64
|
+
const price = item.product.salePrice ?? item.product.price;
|
|
65
|
+
return (
|
|
66
|
+
<div key={`${item.product._id}-${item.variant || 'novariant'}-${idx}`}>
|
|
67
|
+
<span>
|
|
68
|
+
{item.product.name} x{item.qty}
|
|
69
|
+
{item.variant ? ` (${item.variant})` : ''}
|
|
70
|
+
</span>
|
|
71
|
+
<span>₹{price * item.qty}</span>
|
|
72
|
+
<button onClick={() => remove(item.product._id, item.variant)}>Remove</button>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
})}
|
|
57
76
|
<p>Total: ₹{total}</p>
|
|
58
77
|
<button onClick={onCheckout}>Checkout</button>
|
|
59
78
|
</div>
|
|
@@ -84,15 +103,33 @@ export default function Checkout({ cart, onSuccess }) {
|
|
|
84
103
|
|
|
85
104
|
const placeOrder = async () => {
|
|
86
105
|
await loadRazorpay();
|
|
87
|
-
|
|
106
|
+
|
|
107
|
+
// Map cart line items to the orderItems shape the API expects.
|
|
108
|
+
// Critical fields per the SDK README:
|
|
109
|
+
// - `product` (NOT `productId`) — the product _id
|
|
110
|
+
// - `name`, `image` — required, the server doesn't re-fetch them
|
|
111
|
+
// - `variant` — the size STRING, must match what was added to cart
|
|
112
|
+
// or the variant lookup returns undefined and you'll
|
|
113
|
+
// see "INSUFFICIENT_STOCK: undefined: need 1, have 0"
|
|
114
|
+
const items = cart.cart || [];
|
|
115
|
+
const orderItems = items.map(i => ({
|
|
116
|
+
product: i.product._id,
|
|
117
|
+
name: i.product.name,
|
|
118
|
+
image: i.product.images?.[0] || i.product.image,
|
|
119
|
+
qty: i.qty,
|
|
120
|
+
price: i.product.salePrice ?? i.product.price,
|
|
121
|
+
variant: i.variant,
|
|
122
|
+
}));
|
|
123
|
+
const total = orderItems.reduce((s, i) => s + i.price * i.qty, 0);
|
|
88
124
|
|
|
89
125
|
// ONE call to /customer/orders is enough — the server creates the
|
|
90
126
|
// EpicMerch order AND the Razorpay order in a single transaction and
|
|
91
127
|
// returns both back. Do NOT separately call store.payment.createOrder
|
|
92
|
-
// here; doing so creates an orphan second
|
|
93
|
-
// trigger an idempotency-key collision against
|
|
128
|
+
// or store.payment.getConfig here; doing so creates an orphan second
|
|
129
|
+
// Razorpay order AND can trigger an idempotency-key collision against
|
|
130
|
+
// /customer/orders.
|
|
94
131
|
const orderResult = await store.orders.create({
|
|
95
|
-
orderItems
|
|
132
|
+
orderItems,
|
|
96
133
|
shippingAddress: address,
|
|
97
134
|
paymentMethod: 'Razorpay',
|
|
98
135
|
totalPrice: total,
|
|
@@ -29,13 +29,46 @@ export const store = new EpicMerch({
|
|
|
29
29
|
If `src/components/ProductCard.jsx` already exists, skip. Otherwise write:
|
|
30
30
|
|
|
31
31
|
```jsx
|
|
32
|
+
import { useState } from 'react';
|
|
33
|
+
|
|
32
34
|
export default function ProductCard({ product, onAddToCart }) {
|
|
35
|
+
// Variants are stock-tracked per-size. Only show sizes that are in stock —
|
|
36
|
+
// the API will reject orders for out-of-stock variants with
|
|
37
|
+
// "INSUFFICIENT_STOCK: <size>: need N, have 0".
|
|
38
|
+
const availableSizes = (product.variants || []).filter(v => v.stock > 0);
|
|
39
|
+
const [size, setSize] = useState(availableSizes[0]?.size);
|
|
40
|
+
|
|
41
|
+
// Products on sale return both `price` (original) and `salePrice` (current).
|
|
42
|
+
// Always display the current price the customer will actually pay.
|
|
43
|
+
const price = product.salePrice ?? product.price;
|
|
44
|
+
const image = product.images?.[0] || product.image;
|
|
45
|
+
const needsSize = availableSizes.length > 0;
|
|
46
|
+
|
|
33
47
|
return (
|
|
34
48
|
<div className="product-card">
|
|
35
|
-
<img src={
|
|
49
|
+
<img src={image} alt={product.name} />
|
|
36
50
|
<h3>{product.name}</h3>
|
|
37
|
-
<p>₹{
|
|
38
|
-
|
|
51
|
+
<p>₹{price}</p>
|
|
52
|
+
{needsSize && (
|
|
53
|
+
<div className="size-picker">
|
|
54
|
+
{availableSizes.map(v => (
|
|
55
|
+
<button
|
|
56
|
+
key={v.size}
|
|
57
|
+
type="button"
|
|
58
|
+
onClick={() => setSize(v.size)}
|
|
59
|
+
className={size === v.size ? 'selected' : ''}
|
|
60
|
+
>
|
|
61
|
+
{v.size}
|
|
62
|
+
</button>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
<button
|
|
67
|
+
onClick={() => onAddToCart(product._id, size)}
|
|
68
|
+
disabled={needsSize && !size}
|
|
69
|
+
>
|
|
70
|
+
Add to Cart
|
|
71
|
+
</button>
|
|
39
72
|
</div>
|
|
40
73
|
);
|
|
41
74
|
}
|
|
@@ -66,7 +99,10 @@ export default function ProductList() {
|
|
|
66
99
|
}
|
|
67
100
|
}, [query, category]);
|
|
68
101
|
|
|
69
|
-
|
|
102
|
+
// `variant` is the size STRING (e.g. 'L'), per the SDK signature
|
|
103
|
+
// store.cart.add(productId, qty, variant). Products without variants
|
|
104
|
+
// pass undefined and the SDK accepts that.
|
|
105
|
+
const addToCart = (productId, variant) => store.cart.add(productId, 1, variant);
|
|
70
106
|
|
|
71
107
|
return (
|
|
72
108
|
<div>
|
|
@@ -118,13 +118,46 @@ For each of the two files in this step, check existence individually. If a file
|
|
|
118
118
|
Write `src/components/ProductCard.jsx`:
|
|
119
119
|
|
|
120
120
|
```jsx
|
|
121
|
+
import { useState } from 'react';
|
|
122
|
+
|
|
121
123
|
export default function ProductCard({ product, onAddToCart }) {
|
|
124
|
+
// Variants are stock-tracked per-size. Only show sizes that are in stock —
|
|
125
|
+
// the API will reject orders for out-of-stock variants with
|
|
126
|
+
// "INSUFFICIENT_STOCK: <size>: need N, have 0".
|
|
127
|
+
const availableSizes = (product.variants || []).filter(v => v.stock > 0);
|
|
128
|
+
const [size, setSize] = useState(availableSizes[0]?.size);
|
|
129
|
+
|
|
130
|
+
// Products on sale return both `price` (original) and `salePrice` (current).
|
|
131
|
+
// Always display the current price the customer will actually pay.
|
|
132
|
+
const price = product.salePrice ?? product.price;
|
|
133
|
+
const image = product.images?.[0] || product.image;
|
|
134
|
+
const needsSize = availableSizes.length > 0;
|
|
135
|
+
|
|
122
136
|
return (
|
|
123
137
|
<div className="product-card">
|
|
124
|
-
<img src={
|
|
138
|
+
<img src={image} alt={product.name} />
|
|
125
139
|
<h3>{product.name}</h3>
|
|
126
|
-
<p>₹{
|
|
127
|
-
|
|
140
|
+
<p>₹{price}</p>
|
|
141
|
+
{needsSize && (
|
|
142
|
+
<div className="size-picker">
|
|
143
|
+
{availableSizes.map(v => (
|
|
144
|
+
<button
|
|
145
|
+
key={v.size}
|
|
146
|
+
type="button"
|
|
147
|
+
onClick={() => setSize(v.size)}
|
|
148
|
+
className={size === v.size ? 'selected' : ''}
|
|
149
|
+
>
|
|
150
|
+
{v.size}
|
|
151
|
+
</button>
|
|
152
|
+
))}
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
<button
|
|
156
|
+
onClick={() => onAddToCart(product._id, size)}
|
|
157
|
+
disabled={needsSize && !size}
|
|
158
|
+
>
|
|
159
|
+
Add to Cart
|
|
160
|
+
</button>
|
|
128
161
|
</div>
|
|
129
162
|
);
|
|
130
163
|
}
|
|
@@ -153,7 +186,10 @@ export default function ProductList() {
|
|
|
153
186
|
}
|
|
154
187
|
}, [query, category]);
|
|
155
188
|
|
|
156
|
-
|
|
189
|
+
// `variant` is the size STRING (e.g. 'L'), per the SDK signature
|
|
190
|
+
// store.cart.add(productId, qty, variant). Products without variants
|
|
191
|
+
// pass undefined and the SDK accepts that.
|
|
192
|
+
const addToCart = (productId, variant) => store.cart.add(productId, 1, variant);
|
|
157
193
|
|
|
158
194
|
return (
|
|
159
195
|
<div>
|
|
@@ -216,26 +252,46 @@ import { useEffect, useState } from 'react';
|
|
|
216
252
|
import { store } from '../lib/epicmerch';
|
|
217
253
|
|
|
218
254
|
export default function Cart({ onCheckout }) {
|
|
219
|
-
|
|
255
|
+
// store.cart.get() returns { cart, cartCount, cartTotal } — the cart line
|
|
256
|
+
// items are on `cart.cart`, NOT `cart.items`. Each line item is shaped
|
|
257
|
+
// { product: { _id, name, price, salePrice, images, ... }, qty, variant }.
|
|
258
|
+
const [cart, setCart] = useState({ cart: [] });
|
|
220
259
|
|
|
221
260
|
useEffect(() => { store.cart.get().then(setCart); }, []);
|
|
222
261
|
|
|
223
|
-
const remove = async (productId) => {
|
|
224
|
-
await store.cart.remove(productId);
|
|
225
|
-
|
|
262
|
+
const remove = async (productId, variant) => {
|
|
263
|
+
await store.cart.remove(productId, variant);
|
|
264
|
+
// Optimistic local update — match on BOTH productId AND variant so we
|
|
265
|
+
// don't accidentally remove a different size of the same product.
|
|
266
|
+
setCart(c => ({
|
|
267
|
+
...c,
|
|
268
|
+
cart: (c.cart || []).filter(
|
|
269
|
+
i => !(i.product._id === productId && i.variant === variant),
|
|
270
|
+
),
|
|
271
|
+
}));
|
|
226
272
|
};
|
|
227
273
|
|
|
228
|
-
const
|
|
274
|
+
const items = cart.cart || [];
|
|
275
|
+
const total = items.reduce((sum, i) => {
|
|
276
|
+
const price = i.product.salePrice ?? i.product.price;
|
|
277
|
+
return sum + price * i.qty;
|
|
278
|
+
}, 0);
|
|
229
279
|
|
|
230
280
|
return (
|
|
231
281
|
<div>
|
|
232
|
-
{
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
<
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
282
|
+
{items.map((item, idx) => {
|
|
283
|
+
const price = item.product.salePrice ?? item.product.price;
|
|
284
|
+
return (
|
|
285
|
+
<div key={`${item.product._id}-${item.variant || 'novariant'}-${idx}`}>
|
|
286
|
+
<span>
|
|
287
|
+
{item.product.name} x{item.qty}
|
|
288
|
+
{item.variant ? ` (${item.variant})` : ''}
|
|
289
|
+
</span>
|
|
290
|
+
<span>₹{price * item.qty}</span>
|
|
291
|
+
<button onClick={() => remove(item.product._id, item.variant)}>Remove</button>
|
|
292
|
+
</div>
|
|
293
|
+
);
|
|
294
|
+
})}
|
|
239
295
|
<p>Total: ₹{total}</p>
|
|
240
296
|
<button onClick={onCheckout}>Checkout</button>
|
|
241
297
|
</div>
|
|
@@ -249,6 +305,8 @@ Write `src/components/Checkout.jsx`:
|
|
|
249
305
|
import { useState } from 'react';
|
|
250
306
|
import { store } from '../lib/epicmerch';
|
|
251
307
|
|
|
308
|
+
// Razorpay's checkout.js is heavy — load it lazily on the first Place Order
|
|
309
|
+
// click instead of blocking initial page render.
|
|
252
310
|
const loadRazorpay = () =>
|
|
253
311
|
new Promise((resolve) => {
|
|
254
312
|
if (window.Razorpay) return resolve(true);
|
|
@@ -264,23 +322,49 @@ export default function Checkout({ cart, onSuccess }) {
|
|
|
264
322
|
|
|
265
323
|
const placeOrder = async () => {
|
|
266
324
|
await loadRazorpay();
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
325
|
+
|
|
326
|
+
// Map cart line items to the orderItems shape the API expects.
|
|
327
|
+
// Critical fields per the SDK README:
|
|
328
|
+
// - `product` (NOT `productId`) — the product _id
|
|
329
|
+
// - `name`, `image` — required, the server doesn't re-fetch them
|
|
330
|
+
// - `variant` — the size STRING, must match what was added to cart
|
|
331
|
+
// or the variant lookup returns undefined and you'll
|
|
332
|
+
// see "INSUFFICIENT_STOCK: undefined: need 1, have 0"
|
|
333
|
+
const items = cart.cart || [];
|
|
334
|
+
const orderItems = items.map(i => ({
|
|
335
|
+
product: i.product._id,
|
|
336
|
+
name: i.product.name,
|
|
337
|
+
image: i.product.images?.[0] || i.product.image,
|
|
338
|
+
qty: i.qty,
|
|
339
|
+
price: i.product.salePrice ?? i.product.price,
|
|
340
|
+
variant: i.variant,
|
|
341
|
+
}));
|
|
342
|
+
const total = orderItems.reduce((s, i) => s + i.price * i.qty, 0);
|
|
343
|
+
|
|
344
|
+
// ONE call to /customer/orders is enough — the server creates the
|
|
345
|
+
// EpicMerch order AND the Razorpay order in a single transaction and
|
|
346
|
+
// returns both back. Do NOT separately call store.payment.getConfig()
|
|
347
|
+
// or store.payment.createOrder() — orderResult already includes
|
|
348
|
+
// razorpayKeyId, razorpayOrderId, amount, currency, merchantName.
|
|
349
|
+
// Calling them again creates an orphan second Razorpay order AND can
|
|
350
|
+
// trigger an idempotency-key collision against /customer/orders.
|
|
351
|
+
const orderResult = await store.orders.create({
|
|
352
|
+
orderItems,
|
|
270
353
|
shippingAddress: address,
|
|
271
354
|
paymentMethod: 'Razorpay',
|
|
272
355
|
totalPrice: total,
|
|
273
356
|
});
|
|
274
|
-
|
|
275
|
-
const { razorpayOrderId } = await store.payment.createOrder(total * 100, orderId);
|
|
357
|
+
|
|
276
358
|
const rzp = new window.Razorpay({
|
|
277
|
-
key:
|
|
278
|
-
order_id: razorpayOrderId,
|
|
279
|
-
amount:
|
|
359
|
+
key: orderResult.razorpayKeyId,
|
|
360
|
+
order_id: orderResult.razorpayOrderId,
|
|
361
|
+
amount: orderResult.amount,
|
|
362
|
+
currency: orderResult.currency,
|
|
363
|
+
name: orderResult.merchantName,
|
|
280
364
|
handler: async (response) => {
|
|
281
|
-
await store.payment.verify({ ...response, orderId });
|
|
365
|
+
await store.payment.verify({ ...response, orderId: orderResult.orderId });
|
|
282
366
|
await store.cart.clear();
|
|
283
|
-
onSuccess(orderId);
|
|
367
|
+
onSuccess(orderResult.orderId);
|
|
284
368
|
},
|
|
285
369
|
});
|
|
286
370
|
rzp.open();
|
|
@@ -297,6 +381,8 @@ export default function Checkout({ cart, onSuccess }) {
|
|
|
297
381
|
}
|
|
298
382
|
```
|
|
299
383
|
|
|
384
|
+
**If the merchant has a top-level `App.jsx` (or similar) that maintains a `refreshCartCount` or `handleAddToCart`, also update it:** `cart.get()` returns `{ cart, cartCount, cartTotal }`, so `refreshCartCount` should read `c?.cart?.length` (or just `c?.cartCount`), NOT `c?.items?.length`; and `handleAddToCart` should accept a `variant` arg and forward it: `store.cart.add(productId, 1, variant)`.
|
|
385
|
+
|
|
300
386
|
Do NOT add any Razorpay script tag to `index.html` — the script loads dynamically only when the user clicks Place Order.
|
|
301
387
|
|
|
302
388
|
Write `src/components/OrderHistory.jsx`:
|
package/src/index.js
CHANGED
|
@@ -36,7 +36,15 @@ async function main() {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
if (cmd === 'setup') {
|
|
39
|
-
const
|
|
39
|
+
const flagSet = new Set(argv);
|
|
40
|
+
const code = await setup({
|
|
41
|
+
storeName,
|
|
42
|
+
// --force / --reset: re-login even if a valid token exists AND
|
|
43
|
+
// overwrite any existing MCP config + skill files. Use this for
|
|
44
|
+
// a clean from-scratch reinstall (e.g. when testing a fresh
|
|
45
|
+
// merchant onboarding flow).
|
|
46
|
+
force: flagSet.has('--force') || flagSet.has('--reset'),
|
|
47
|
+
});
|
|
40
48
|
process.exit(code);
|
|
41
49
|
return;
|
|
42
50
|
}
|
package/src/install.js
CHANGED
|
@@ -344,7 +344,10 @@ export async function install(opts = {}) {
|
|
|
344
344
|
* (MCP config + slash commands). The merchant-friendly path; no
|
|
345
345
|
* filesystem knowledge required.
|
|
346
346
|
*
|
|
347
|
-
* @param {{ storeName?: string, force?: boolean }} opts
|
|
347
|
+
* @param {{ storeName?: string, force?: boolean }} opts - `force` re-runs
|
|
348
|
+
* the browser login even if a valid token already exists, AND overwrites
|
|
349
|
+
* any existing MCP config + skill files. Useful for from-scratch
|
|
350
|
+
* reinstalls or testing the onboarding flow as a new merchant.
|
|
348
351
|
* @returns {Promise<number>} exit code
|
|
349
352
|
*/
|
|
350
353
|
export async function setup(opts = {}) {
|
|
@@ -352,20 +355,32 @@ export async function setup(opts = {}) {
|
|
|
352
355
|
console.error('━━━ EpicMerch setup ━━━');
|
|
353
356
|
console.error('');
|
|
354
357
|
|
|
355
|
-
// --- Step 1: Login (skipped if a valid token already exists) ---
|
|
358
|
+
// --- Step 1: Login (skipped if a valid token already exists, UNLESS --force) ---
|
|
356
359
|
// ESM dynamic import of an ESM-mocked token-store needs the same module specifier
|
|
357
360
|
// as the test mocks; the helper above uses createRequire under the hood when needed.
|
|
358
|
-
const
|
|
361
|
+
const tokenStore = await import('./token-store.js');
|
|
362
|
+
const hasValidToken = (() => {
|
|
359
363
|
try {
|
|
360
|
-
const data =
|
|
364
|
+
const data = tokenStore.readTokenStore();
|
|
361
365
|
const entry = data?.stores?.[data?.defaultStore];
|
|
362
366
|
return entry && new Date(entry.refreshTokenExpiresAt) > new Date();
|
|
363
367
|
} catch (_) { return false; }
|
|
364
|
-
});
|
|
368
|
+
})();
|
|
369
|
+
const skipLogin = hasValidToken && !opts.force;
|
|
365
370
|
|
|
366
371
|
if (skipLogin) {
|
|
367
372
|
console.error('Step 1/2: ✓ Already authenticated (skipping browser login).');
|
|
373
|
+
console.error(' Pass --force to re-login.');
|
|
368
374
|
} else {
|
|
375
|
+
if (hasValidToken && opts.force) {
|
|
376
|
+
// --force on an authenticated session: revoke + remove the stored
|
|
377
|
+
// token first so the next login is genuinely fresh (and the server
|
|
378
|
+
// sees a new device row, not a renewed one).
|
|
379
|
+
try {
|
|
380
|
+
const { logout } = await import('./logout.js');
|
|
381
|
+
await logout({ storeName });
|
|
382
|
+
} catch (_) { /* don't block setup if revoke fails */ }
|
|
383
|
+
}
|
|
369
384
|
console.error('Step 1/2: Authenticating via browser…');
|
|
370
385
|
console.error('');
|
|
371
386
|
try {
|
package/src/tools/scaffold.js
CHANGED
|
@@ -1,285 +1,372 @@
|
|
|
1
|
-
// src/tools/scaffold.js
|
|
2
|
-
const TEMPLATES = {
|
|
3
|
-
react: {
|
|
4
|
-
auth: [
|
|
5
|
-
{
|
|
6
|
-
path: 'src/lib/epicmerch.js',
|
|
7
|
-
content: `import EpicMerch from 'epicmerch';
|
|
8
|
-
|
|
9
|
-
export const store = new EpicMerch({
|
|
10
|
-
apiKey: import.meta.env.VITE_API_KEY,
|
|
11
|
-
...(import.meta.env.VITE_API_URL && { baseUrl: import.meta.env.VITE_API_URL }),
|
|
12
|
-
});
|
|
13
|
-
`,
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
path: 'src/components/Login.jsx',
|
|
17
|
-
content: `import { useState } from 'react';
|
|
18
|
-
import { store } from '../lib/epicmerch';
|
|
19
|
-
|
|
20
|
-
export default function Login({ onLogin }) {
|
|
21
|
-
const [phone, setPhone] = useState('');
|
|
22
|
-
const [otp, setOtp] = useState('');
|
|
23
|
-
const [step, setStep] = useState('phone');
|
|
24
|
-
|
|
25
|
-
const sendOtp = async () => {
|
|
26
|
-
await store.auth.sendOtp(phone, 'phone');
|
|
27
|
-
setStep('otp');
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const verify = async () => {
|
|
31
|
-
const result = await store.auth.verifyOtp(phone, otp, {});
|
|
32
|
-
if (result.token) {
|
|
33
|
-
store.setCustomerToken(result.token);
|
|
34
|
-
localStorage.setItem('customerInfo', JSON.stringify(result));
|
|
35
|
-
onLogin(result);
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<div>
|
|
41
|
-
{step === 'phone' ? (
|
|
42
|
-
<>
|
|
43
|
-
<input value={phone} onChange={e => setPhone(e.target.value)} placeholder="+919876543210" />
|
|
44
|
-
<button onClick={sendOtp}>Send OTP</button>
|
|
45
|
-
</>
|
|
46
|
-
) : (
|
|
47
|
-
<>
|
|
48
|
-
<input value={otp} onChange={e => setOtp(e.target.value)} placeholder="Enter OTP" />
|
|
49
|
-
<button onClick={verify}>Verify</button>
|
|
50
|
-
</>
|
|
51
|
-
)}
|
|
52
|
-
</div>
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
`,
|
|
56
|
-
},
|
|
57
|
-
],
|
|
58
|
-
|
|
59
|
-
products: [
|
|
60
|
-
{
|
|
61
|
-
path: 'src/components/ProductCard.jsx',
|
|
62
|
-
content: `
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
import {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
1
|
+
// src/tools/scaffold.js
|
|
2
|
+
const TEMPLATES = {
|
|
3
|
+
react: {
|
|
4
|
+
auth: [
|
|
5
|
+
{
|
|
6
|
+
path: 'src/lib/epicmerch.js',
|
|
7
|
+
content: `import EpicMerch from 'epicmerch';
|
|
8
|
+
|
|
9
|
+
export const store = new EpicMerch({
|
|
10
|
+
apiKey: import.meta.env.VITE_API_KEY,
|
|
11
|
+
...(import.meta.env.VITE_API_URL && { baseUrl: import.meta.env.VITE_API_URL }),
|
|
12
|
+
});
|
|
13
|
+
`,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
path: 'src/components/Login.jsx',
|
|
17
|
+
content: `import { useState } from 'react';
|
|
18
|
+
import { store } from '../lib/epicmerch';
|
|
19
|
+
|
|
20
|
+
export default function Login({ onLogin }) {
|
|
21
|
+
const [phone, setPhone] = useState('');
|
|
22
|
+
const [otp, setOtp] = useState('');
|
|
23
|
+
const [step, setStep] = useState('phone');
|
|
24
|
+
|
|
25
|
+
const sendOtp = async () => {
|
|
26
|
+
await store.auth.sendOtp(phone, 'phone');
|
|
27
|
+
setStep('otp');
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const verify = async () => {
|
|
31
|
+
const result = await store.auth.verifyOtp(phone, otp, {});
|
|
32
|
+
if (result.token) {
|
|
33
|
+
store.setCustomerToken(result.token);
|
|
34
|
+
localStorage.setItem('customerInfo', JSON.stringify(result));
|
|
35
|
+
onLogin(result);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div>
|
|
41
|
+
{step === 'phone' ? (
|
|
42
|
+
<>
|
|
43
|
+
<input value={phone} onChange={e => setPhone(e.target.value)} placeholder="+919876543210" />
|
|
44
|
+
<button onClick={sendOtp}>Send OTP</button>
|
|
45
|
+
</>
|
|
46
|
+
) : (
|
|
47
|
+
<>
|
|
48
|
+
<input value={otp} onChange={e => setOtp(e.target.value)} placeholder="Enter OTP" />
|
|
49
|
+
<button onClick={verify}>Verify</button>
|
|
50
|
+
</>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
`,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
|
|
59
|
+
products: [
|
|
60
|
+
{
|
|
61
|
+
path: 'src/components/ProductCard.jsx',
|
|
62
|
+
content: `import { useState } from 'react';
|
|
63
|
+
|
|
64
|
+
export default function ProductCard({ product, onAddToCart }) {
|
|
65
|
+
// Variants are stock-tracked per-size. Only show sizes that are in stock —
|
|
66
|
+
// the API will reject orders for out-of-stock variants with
|
|
67
|
+
// "INSUFFICIENT_STOCK: <size>: need N, have 0".
|
|
68
|
+
const availableSizes = (product.variants || []).filter(v => v.stock > 0);
|
|
69
|
+
const [size, setSize] = useState(availableSizes[0]?.size);
|
|
70
|
+
|
|
71
|
+
// Products on sale return both \`price\` (original) and \`salePrice\` (current).
|
|
72
|
+
// Always display the current price the customer will actually pay.
|
|
73
|
+
const price = product.salePrice ?? product.price;
|
|
74
|
+
const image = product.images?.[0] || product.image;
|
|
75
|
+
const needsSize = availableSizes.length > 0;
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="product-card">
|
|
79
|
+
<img src={image} alt={product.name} />
|
|
80
|
+
<h3>{product.name}</h3>
|
|
81
|
+
<p>₹{price}</p>
|
|
82
|
+
{needsSize && (
|
|
83
|
+
<div className="size-picker">
|
|
84
|
+
{availableSizes.map(v => (
|
|
85
|
+
<button
|
|
86
|
+
key={v.size}
|
|
87
|
+
type="button"
|
|
88
|
+
onClick={() => setSize(v.size)}
|
|
89
|
+
className={size === v.size ? 'selected' : ''}
|
|
90
|
+
>
|
|
91
|
+
{v.size}
|
|
92
|
+
</button>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
<button
|
|
97
|
+
onClick={() => onAddToCart(product._id, size)}
|
|
98
|
+
disabled={needsSize && !size}
|
|
99
|
+
>
|
|
100
|
+
Add to Cart
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
`,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
path: 'src/components/ProductList.jsx',
|
|
109
|
+
content: `import { useEffect, useState } from 'react';
|
|
110
|
+
import { store } from '../lib/epicmerch';
|
|
111
|
+
import ProductCard from './ProductCard';
|
|
112
|
+
|
|
113
|
+
export default function ProductList() {
|
|
114
|
+
const [products, setProducts] = useState([]);
|
|
115
|
+
const [category, setCategory] = useState(undefined);
|
|
116
|
+
const [categories, setCategories] = useState([]);
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
store.categories.list().then(setCategories);
|
|
120
|
+
}, []);
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
store.products.list({ type: category, limit: 12 }).then(d => setProducts(d.products ?? d));
|
|
124
|
+
}, [category]);
|
|
125
|
+
|
|
126
|
+
// \`variant\` is the size STRING (e.g. 'L'), per the SDK signature
|
|
127
|
+
// store.cart.add(productId, qty, variant). Products without variants
|
|
128
|
+
// pass undefined and the SDK accepts that.
|
|
129
|
+
const addToCart = async (productId, variant) => {
|
|
130
|
+
await store.cart.add(productId, 1, variant);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div>
|
|
135
|
+
<div>
|
|
136
|
+
{categories.map(c => (
|
|
137
|
+
<button key={c} onClick={() => setCategory(c === 'All' ? undefined : c)}>{c}</button>
|
|
138
|
+
))}
|
|
139
|
+
</div>
|
|
140
|
+
<div className="product-grid">
|
|
141
|
+
{products.map(p => <ProductCard key={p._id} product={p} onAddToCart={addToCart} />)}
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
`,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
|
|
150
|
+
orders: [
|
|
151
|
+
{
|
|
152
|
+
path: 'src/components/Cart.jsx',
|
|
153
|
+
content: `import { useEffect, useState } from 'react';
|
|
154
|
+
import { store } from '../lib/epicmerch';
|
|
155
|
+
|
|
156
|
+
export default function Cart({ onCheckout }) {
|
|
157
|
+
// store.cart.get() returns { cart, cartCount, cartTotal } — the cart line
|
|
158
|
+
// items are on \`cart.cart\`, NOT \`cart.items\`. Each line item is shaped
|
|
159
|
+
// { product: { _id, name, price, salePrice, images, ... }, qty, variant }.
|
|
160
|
+
const [cart, setCart] = useState({ cart: [] });
|
|
161
|
+
|
|
162
|
+
useEffect(() => { store.cart.get().then(setCart); }, []);
|
|
163
|
+
|
|
164
|
+
const remove = async (productId, variant) => {
|
|
165
|
+
await store.cart.remove(productId, variant);
|
|
166
|
+
// Optimistic local update — match on BOTH productId and variant so we
|
|
167
|
+
// don't accidentally remove a different size of the same product.
|
|
168
|
+
setCart(c => ({
|
|
169
|
+
...c,
|
|
170
|
+
cart: (c.cart || []).filter(
|
|
171
|
+
i => !(i.product._id === productId && i.variant === variant),
|
|
172
|
+
),
|
|
173
|
+
}));
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const items = cart.cart || [];
|
|
177
|
+
const total = items.reduce((sum, i) => {
|
|
178
|
+
const price = i.product.salePrice ?? i.product.price;
|
|
179
|
+
return sum + price * i.qty;
|
|
180
|
+
}, 0);
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div>
|
|
184
|
+
{items.map((item, idx) => {
|
|
185
|
+
const price = item.product.salePrice ?? item.product.price;
|
|
186
|
+
return (
|
|
187
|
+
<div key={\`\${item.product._id}-\${item.variant || 'novariant'}-\${idx}\`}>
|
|
188
|
+
<span>
|
|
189
|
+
{item.product.name} x{item.qty}
|
|
190
|
+
{item.variant ? \` (\${item.variant})\` : ''}
|
|
191
|
+
</span>
|
|
192
|
+
<span>₹{price * item.qty}</span>
|
|
193
|
+
<button onClick={() => remove(item.product._id, item.variant)}>Remove</button>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
})}
|
|
197
|
+
<p>Total: ₹{total}</p>
|
|
198
|
+
<button onClick={onCheckout}>Checkout</button>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
`,
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
path: 'src/components/Checkout.jsx',
|
|
206
|
+
content: `import { useState } from 'react';
|
|
207
|
+
import { store } from '../lib/epicmerch';
|
|
208
|
+
|
|
209
|
+
// Razorpay's checkout.js is heavy — load it lazily on the first Place Order
|
|
210
|
+
// click instead of blocking initial page render. window.Razorpay sticks
|
|
211
|
+
// around after first load, so subsequent clicks skip the network round-trip.
|
|
212
|
+
const loadRazorpay = () =>
|
|
213
|
+
new Promise((resolve) => {
|
|
214
|
+
if (window.Razorpay) return resolve(true);
|
|
215
|
+
const script = document.createElement('script');
|
|
216
|
+
script.src = 'https://checkout.razorpay.com/v1/checkout.js';
|
|
217
|
+
script.onload = () => resolve(true);
|
|
218
|
+
script.onerror = () => resolve(false);
|
|
219
|
+
document.body.appendChild(script);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
export default function Checkout({ cart, onSuccess }) {
|
|
223
|
+
const [address, setAddress] = useState({ street: '', city: '', postalCode: '', country: 'India' });
|
|
224
|
+
|
|
225
|
+
const placeOrder = async () => {
|
|
226
|
+
await loadRazorpay();
|
|
227
|
+
|
|
228
|
+
// Map cart line items to the orderItems shape the API expects.
|
|
229
|
+
// Critical fields per the SDK README:
|
|
230
|
+
// - \`product\` (NOT \`productId\`) — the product _id
|
|
231
|
+
// - \`name\`, \`image\` — required, the server doesn't re-fetch them
|
|
232
|
+
// - \`variant\` — the size string, must match what was added to cart
|
|
233
|
+
// or the variant lookup returns undefined and you'll
|
|
234
|
+
// see "INSUFFICIENT_STOCK: undefined: need 1, have 0"
|
|
235
|
+
const items = cart.cart || [];
|
|
236
|
+
const orderItems = items.map(i => ({
|
|
237
|
+
product: i.product._id,
|
|
238
|
+
name: i.product.name,
|
|
239
|
+
image: i.product.images?.[0] || i.product.image,
|
|
240
|
+
qty: i.qty,
|
|
241
|
+
price: i.product.salePrice ?? i.product.price,
|
|
242
|
+
variant: i.variant,
|
|
243
|
+
}));
|
|
244
|
+
const total = orderItems.reduce((s, i) => s + i.price * i.qty, 0);
|
|
245
|
+
|
|
246
|
+
// ONE call to /customer/orders. The server creates the EpicMerch order
|
|
247
|
+
// AND the Razorpay order in a single transaction and returns both in
|
|
248
|
+
// the response. Do NOT separately call payment.getConfig() or
|
|
249
|
+
// payment.createOrder() — orderResult already includes razorpayKeyId,
|
|
250
|
+
// razorpayOrderId, amount, currency, and merchantName. Calling them
|
|
251
|
+
// again creates an orphan second Razorpay order AND triggers an
|
|
252
|
+
// idempotency-key collision against this same response cache.
|
|
253
|
+
const orderResult = await store.orders.create({
|
|
254
|
+
orderItems,
|
|
255
|
+
shippingAddress: address,
|
|
256
|
+
paymentMethod: 'Razorpay',
|
|
257
|
+
totalPrice: total,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const rzp = new window.Razorpay({
|
|
261
|
+
key: orderResult.razorpayKeyId,
|
|
262
|
+
order_id: orderResult.razorpayOrderId,
|
|
263
|
+
amount: orderResult.amount,
|
|
264
|
+
currency: orderResult.currency,
|
|
265
|
+
name: orderResult.merchantName,
|
|
266
|
+
handler: async (response) => {
|
|
267
|
+
await store.payment.verify({ ...response, orderId: orderResult.orderId });
|
|
268
|
+
await store.cart.clear();
|
|
269
|
+
onSuccess(orderResult.orderId);
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
rzp.open();
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<div>
|
|
277
|
+
<input placeholder="Street" onChange={e => setAddress(a => ({ ...a, street: e.target.value }))} />
|
|
278
|
+
<input placeholder="City" onChange={e => setAddress(a => ({ ...a, city: e.target.value }))} />
|
|
279
|
+
<input placeholder="Postal Code" onChange={e => setAddress(a => ({ ...a, postalCode: e.target.value }))} />
|
|
280
|
+
<button onClick={placeOrder}>Place Order</button>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
`,
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
path: 'src/components/OrderHistory.jsx',
|
|
288
|
+
content: `import { useEffect, useState } from 'react';
|
|
289
|
+
import { store } from '../lib/epicmerch';
|
|
290
|
+
|
|
291
|
+
export default function OrderHistory() {
|
|
292
|
+
const [orders, setOrders] = useState([]);
|
|
293
|
+
|
|
294
|
+
useEffect(() => { store.orders.list().then(setOrders); }, []);
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<div>
|
|
298
|
+
{orders.map(order => (
|
|
299
|
+
<div key={order._id}>
|
|
300
|
+
<span>Order #{order._id.slice(-6)}</span>
|
|
301
|
+
<span>{order.status}</span>
|
|
302
|
+
<span>₹{order.totalPrice}</span>
|
|
303
|
+
</div>
|
|
304
|
+
))}
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
`,
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
|
|
312
|
+
envExample: {
|
|
313
|
+
path: '.env.example',
|
|
314
|
+
content: `VITE_API_KEY=your_epicmerch_api_key_here\n# VITE_API_URL=http://localhost:5001/api\n`,
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
function getFrameworkTemplates(framework) {
|
|
320
|
+
if (!TEMPLATES[framework]) {
|
|
321
|
+
throw new Error(`Framework "${framework}" not supported. Supported: ${Object.keys(TEMPLATES).join(', ')}`);
|
|
322
|
+
}
|
|
323
|
+
return TEMPLATES[framework];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function scaffoldTools() {
|
|
327
|
+
return {
|
|
328
|
+
async store_scaffold_auth({ framework = 'react' } = {}) {
|
|
329
|
+
try {
|
|
330
|
+
const t = getFrameworkTemplates(framework);
|
|
331
|
+
return { content: [{ type: 'text', text: JSON.stringify(t.auth, null, 2) }] };
|
|
332
|
+
} catch (err) {
|
|
333
|
+
return { isError: true, content: [{ type: 'text', text: err.message }] };
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
async store_scaffold_products({ framework = 'react' } = {}) {
|
|
338
|
+
try {
|
|
339
|
+
const t = getFrameworkTemplates(framework);
|
|
340
|
+
return { content: [{ type: 'text', text: JSON.stringify(t.products, null, 2) }] };
|
|
341
|
+
} catch (err) {
|
|
342
|
+
return { isError: true, content: [{ type: 'text', text: err.message }] };
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
async store_scaffold_orders({ framework = 'react' } = {}) {
|
|
347
|
+
try {
|
|
348
|
+
const t = getFrameworkTemplates(framework);
|
|
349
|
+
return { content: [{ type: 'text', text: JSON.stringify(t.orders, null, 2) }] };
|
|
350
|
+
} catch (err) {
|
|
351
|
+
return { isError: true, content: [{ type: 'text', text: err.message }] };
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
async store_scaffold_full({ framework = 'react' } = {}) {
|
|
356
|
+
try {
|
|
357
|
+
const t = getFrameworkTemplates(framework);
|
|
358
|
+
const files = [...t.auth, ...t.products, ...t.orders, t.envExample];
|
|
359
|
+
return { content: [{ type: 'text', text: JSON.stringify(files, null, 2) }] };
|
|
360
|
+
} catch (err) {
|
|
361
|
+
return { isError: true, content: [{ type: 'text', text: err.message }] };
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export const scaffoldToolDefs = [
|
|
368
|
+
{ name: 'store_scaffold_auth', description: 'Generate auth integration files (SDK init + Login component) for a framework. Returns [{path, content}] — write these files to the project.', schema: { framework: { type: 'string', description: 'react (default)' } } },
|
|
369
|
+
{ name: 'store_scaffold_products', description: 'Generate product catalog components (ProductList, ProductCard with size picker) for a framework.', schema: { framework: { type: 'string' } } },
|
|
370
|
+
{ name: 'store_scaffold_orders', description: 'Generate cart + order management components (Cart, Checkout, OrderHistory) for a framework. Cart reads the {cart, cartCount, cartTotal} shape returned by store.cart.get(); Checkout uses the orderItems shape {product, name, image, qty, price, variant} required by the API.', schema: { framework: { type: 'string' } } },
|
|
371
|
+
{ name: 'store_scaffold_full', description: 'Generate a complete EpicMerch-integrated storefront scaffold (auth + products + orders + .env.example).', schema: { framework: { type: 'string' } } },
|
|
372
|
+
];
|