epicmerch-mcp 1.1.1 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epicmerch-mcp",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "MCP server for EpicMerch — integrates e-commerce into Claude and ChatGPT",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -39,7 +39,8 @@ export default function Cart({ onCheckout }) {
39
39
 
40
40
  const remove = async (productId) => {
41
41
  await store.cart.remove(productId);
42
- store.cart.get().then(setCart);
42
+ // Optimistic local update — no need to re-fetch the whole cart.
43
+ setCart(c => ({ ...c, items: (c.items || []).filter(i => i.productId !== productId) }));
43
44
  };
44
45
 
45
46
  const total = cart.items?.reduce((s, i) => s + i.price * i.qty, 0) ?? 0;
@@ -84,22 +85,29 @@ export default function Checkout({ cart, onSuccess }) {
84
85
  const placeOrder = async () => {
85
86
  await loadRazorpay();
86
87
  const total = cart.items.reduce((s, i) => s + i.price * i.qty, 0);
87
- const { orderId } = await store.orders.create({
88
+
89
+ // ONE call to /customer/orders is enough — the server creates the
90
+ // EpicMerch order AND the Razorpay order in a single transaction and
91
+ // returns both back. Do NOT separately call store.payment.createOrder
92
+ // here; doing so creates an orphan second Razorpay order AND can
93
+ // trigger an idempotency-key collision against /customer/orders.
94
+ const orderResult = await store.orders.create({
88
95
  orderItems: cart.items.map(i => ({ productId: i.productId, qty: i.qty, price: i.price })),
89
96
  shippingAddress: address,
90
97
  paymentMethod: 'Razorpay',
91
98
  totalPrice: total,
92
99
  });
93
- const config = await store.payment.getConfig();
94
- const { razorpayOrderId } = await store.payment.createOrder(total * 100, orderId);
100
+
95
101
  const rzp = new window.Razorpay({
96
- key: config.razorpayKeyId,
97
- order_id: razorpayOrderId,
98
- amount: total * 100,
102
+ key: orderResult.razorpayKeyId,
103
+ order_id: orderResult.razorpayOrderId,
104
+ amount: orderResult.amount,
105
+ currency: orderResult.currency,
106
+ name: orderResult.merchantName,
99
107
  handler: async (response) => {
100
- await store.payment.verify({ ...response, orderId });
108
+ await store.payment.verify({ ...response, orderId: orderResult.orderId });
101
109
  await store.cart.clear();
102
- onSuccess(orderId);
110
+ onSuccess(orderResult.orderId);
103
111
  },
104
112
  });
105
113
  rzp.open();
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: epicmerch-payments
3
+ description: Set up payments on an EpicMerch store — help the merchant pick between Razorpay (India, default) and Stripe (international), then route to the right setup skill.
4
+ ---
5
+
6
+ # EpicMerch Payments — the umbrella
7
+
8
+ This is the entry point for any "set up payments" / "configure checkout" / "add card payments" intent. Your job is to figure out **which processor the merchant should use**, then hand off to the matching detailed skill.
9
+
10
+ ## Step 1: Check what's already configured
11
+
12
+ Before asking anything, call `merchant_get_checkout_settings()`. The response tells you:
13
+
14
+ - `checkoutType` — current active flow (`"epicmerch"`, `"stripe"`, or `"shiprocket"`)
15
+ - `razorpayKeyId` (string or null) + `razorpayKeySecretIsSet` (bool)
16
+ - `stripePublishableKey` (string or null) + `stripeSecretKeyIsSet` (bool) + `stripeWebhookSecretIsSet` (bool)
17
+
18
+ Decide based on what's there:
19
+
20
+ | Current state | Suggested action |
21
+ |---|---|
22
+ | Nothing configured | Help the merchant pick + set up (continue to Step 2) |
23
+ | Razorpay configured, `checkoutType: epicmerch` | Tell them: "Razorpay is already live on your store. Want to add Stripe as a backup / switch to Stripe?" |
24
+ | Stripe configured, `checkoutType: stripe` | Tell them: "Stripe is already live. Want to switch back to Razorpay?" |
25
+ | Both configured, only one active | Tell them which is active + ask if they want to switch |
26
+
27
+ If something's clearly already set up and they didn't ask to change it, stop here. Don't redo configured work.
28
+
29
+ ## Step 2: Help them pick
30
+
31
+ If nothing is configured (or they want to add a new one), help them choose:
32
+
33
+ | Question | Razorpay | Stripe |
34
+ |---|---|---|
35
+ | Where are most of your customers? | India 🇮🇳 | International 🌍 |
36
+ | Currency you charge in? | INR primarily | USD, EUR, GBP, AUD, etc. |
37
+ | Payment methods you want? | UPI, netbanking, Indian cards, wallets | Cards, Apple Pay, Google Pay, Stripe Link |
38
+ | Compliance | RBI-compliant by default | Stripe handles PCI; you handle GDPR |
39
+ | Setup time | ~5 min if account exists | ~10 min (needs webhook config) |
40
+ | Account onboarding | KYC ~24h (already done usually) | Stripe activation can take a day |
41
+
42
+ Ask the merchant ONE question that disambiguates. Don't make them read the table. The best single question is usually:
43
+
44
+ > "Where are most of your customers — India, or international?"
45
+
46
+ - **India only** → Razorpay
47
+ - **International / mostly outside India** → Stripe
48
+ - **Both / not sure** → ask: "What currency do you want to charge in?" then route accordingly.
49
+
50
+ ## Step 3: Hand off to the specific skill
51
+
52
+ Once you know which one:
53
+
54
+ - **Razorpay** → hand off to `/epicmerch-razorpay`. Don't try to configure it inline; the specific skill has the right Razorpay-dashboard URLs, key format hints, and the activation step.
55
+ - **Stripe** → hand off to `/epicmerch-stripe`. Same reasoning — that skill has the webhook URL, the test recipe, and the activation step.
56
+
57
+ If you're running inside a client that doesn't support slash command handoff (e.g. **Codex**), invoke the same MCP tools yourself:
58
+
59
+ **For Razorpay inline:**
60
+ ```
61
+ merchant_configure_razorpay({ keyId: "rzp_live_...", keySecret: "..." })
62
+ merchant_set_checkout_type({ type: "epicmerch" })
63
+ merchant_get_checkout_settings() // verify
64
+ merchant_diagnose() // confirm readiness
65
+ ```
66
+
67
+ **For Stripe inline:**
68
+ ```
69
+ merchant_configure_stripe({ publishableKey: "pk_live_...", secretKey: "sk_live_...", webhookSecret: "whsec_..." })
70
+ merchant_test_stripe_connection()
71
+ merchant_set_checkout_type({ type: "stripe" })
72
+ ```
73
+
74
+ ## Step 4: After setup
75
+
76
+ Once whichever processor is live, suggest:
77
+
78
+ - "Want to test? Place a small test order on your storefront. With test keys, no real money moves; with live keys, you can refund it after."
79
+ - "Need to set up Shiprocket for shipping? Use `merchant_get_logistics_settings` first to see what's there."
80
+ - "Ready to launch? Run `merchant_diagnose()` — I'll check everything."
81
+
82
+ ## Style
83
+
84
+ - Don't ask "Razorpay or Stripe?" out of context. ALWAYS check current state first (Step 1) — if it's set up, just confirm.
85
+ - Don't pile up questions. One at a time.
86
+ - Never paste the merchant's secret back at them — refer to it as `"the secret you provided"`. Treat secrets as write-only.
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: epicmerch-razorpay
3
+ description: Set up Razorpay payments for an EpicMerch store conversationally — get the Key ID + Secret from Razorpay's dashboard, save them, and activate the EpicMerch Checkout flow.
4
+ ---
5
+
6
+ # EpicMerch Razorpay Setup
7
+
8
+ You're helping a merchant configure Razorpay as their payment processor. This pairs Razorpay with EpicMerch's default checkout flow (Razorpay for payments + Shiprocket for shipping). Walk them through these steps.
9
+
10
+ ## Step 1: Confirm they have a Razorpay account
11
+
12
+ Ask: "Do you already have a Razorpay account at dashboard.razorpay.com? If not, sign up first — it's free and KYC takes ~24 hours."
13
+
14
+ If they're in India and just starting out, Razorpay is the most common choice. For international merchants, hand off to `/epicmerch-stripe` instead.
15
+
16
+ ## Step 2: Decide test vs live mode
17
+
18
+ Ask: "Are you setting up for testing (`rzp_test_...` keys), or going live with real customers (`rzp_live_...`)?"
19
+
20
+ Make sure they understand:
21
+ - **Test keys** — no real money. Use Razorpay's test card numbers from their docs. Switch to live before launching.
22
+ - **Live keys** — real money. Account must be activated (KYC complete + bank account linked).
23
+
24
+ ## Step 3: Collect their API keys
25
+
26
+ Tell them:
27
+
28
+ > "In a new browser tab, open https://dashboard.razorpay.com → Settings → API Keys → Generate Key (or Show, if you've generated one before).
29
+ >
30
+ > Copy these two values:
31
+ > - **Key ID** — looks like `rzp_live_XXXXX` or `rzp_test_XXXXX`
32
+ > - **Key Secret** — long random string. Razorpay only shows it ONCE when you generate. If you've already saved it somewhere safe, paste it now."
33
+
34
+ If the merchant lost their secret, tell them: "You'll need to regenerate the key pair in Razorpay's dashboard. The old Key ID stops working when you do this, so update both fields below."
35
+
36
+ ## Step 4: Save them
37
+
38
+ Call:
39
+
40
+ ```
41
+ merchant_configure_razorpay({
42
+ keyId: "rzp_live_XXXXX",
43
+ keySecret: "your_secret_here"
44
+ })
45
+ ```
46
+
47
+ The server encrypts the secret before storing. The response confirms the save with `razorpayKeyId` echoed back and `razorpayKeySecretIsSet: true`.
48
+
49
+ ## Step 5: Activate the checkout
50
+
51
+ Razorpay only powers payments under EpicMerch's default checkout flow. Activate it:
52
+
53
+ ```
54
+ merchant_set_checkout_type({ type: "epicmerch" })
55
+ ```
56
+
57
+ This sets the storefront to use EpicMerch Checkout (Razorpay payments + Shiprocket shipping). If the merchant was previously on Stripe or Shiprocket-managed checkout, this switches them over.
58
+
59
+ ## Step 6: Verify it took effect
60
+
61
+ Call `merchant_get_checkout_settings()` and confirm the response includes:
62
+ - `checkoutType: "epicmerch"`
63
+ - `razorpayKeyId` matching what you just set
64
+ - `razorpayKeySecretIsSet: true`
65
+
66
+ If those line up, you're done.
67
+
68
+ ## Step 7: Smoke test (optional but recommended)
69
+
70
+ Tell the merchant: "Want me to do a quick health check? I'll look at your full store readiness, including payment config."
71
+
72
+ Then call `merchant_diagnose()`. Read off the `payment.razorpayConfigured` value — should be `true` — and the `readinessScore`.
73
+
74
+ ## Step 8: Done
75
+
76
+ Say:
77
+
78
+ > "Razorpay is now live on your storefront. Customers will see Razorpay's payment widget at checkout — cards, UPI, netbanking, wallets. **Make sure you're using live keys before launch** — test keys won't charge real money."
79
+
80
+ If they're on test keys, remind them again: "When you're ready to launch, repeat Steps 3–4 with live keys."
81
+
82
+ ## Style
83
+
84
+ - Ask one question at a time. Don't dump all the steps at once.
85
+ - After each `merchant_*` call, read back the relevant field so the merchant sees progress.
86
+ - If a save fails (e.g. validation error from the server), surface the error message verbatim — don't paraphrase.
87
+
88
+ ## Troubleshooting
89
+
90
+ If `merchant_get_checkout_settings` shows `razorpayKeySecretIsSet: false` after Step 4, the secret didn't persist. Causes:
91
+ - Empty string passed (Razorpay secrets are 30+ chars; if the merchant pasted nothing, the field stays empty)
92
+ - Encryption key mismatch on the server side (rare; tell the merchant to check with support)
93
+
94
+ If Step 5 errors with "must be one of: ...", you typed the wrong checkout type. Use exactly `"epicmerch"` (lowercase).
95
+
96
+ ## Handoff
97
+
98
+ If the merchant says "actually I want Stripe instead" — switch to `/epicmerch-stripe`. If "what's the difference between Razorpay and Stripe?" — use `/epicmerch-payments` to compare.
@@ -0,0 +1,389 @@
1
+ ---
2
+ name: epicmerch-storefront
3
+ description: One-shot full EpicMerch storefront scaffold — SDK + Login + ProductCard + ProductList + Cart + Checkout + OrderHistory + .env with auto-generated API key. The "do everything" route under the /epicmerch umbrella.
4
+ ---
5
+
6
+ # EpicMerch Full Storefront Scaffold
7
+
8
+ You are integrating EpicMerch into this project. EpicMerch is a complete e-commerce backend that handles auth, inventory, orders, and payments. Follow every step below in order without asking for confirmation unless a step explicitly says to ask.
9
+
10
+ **Idempotency rule (apply throughout):** Before writing any file in the scaffold steps below, check if it already exists. If it does, SKIP that file and tell the user "X already exists — keeping it". Never overwrite existing files. The merchant may already have a polished version of any of these components; preserving their work is more important than completing the scaffold.
11
+
12
+ ## Checklist
13
+
14
+ - [ ] **Step 0: Survey the project**
15
+
16
+ List which of the scaffold target files already exist in this project:
17
+ - `src/lib/epicmerch.js`
18
+ - `src/components/Login.jsx`
19
+ - `src/components/ProductCard.jsx`
20
+ - `src/components/ProductList.jsx`
21
+ - `src/components/Cart.jsx`
22
+ - `src/components/Checkout.jsx`
23
+ - `src/components/OrderHistory.jsx`
24
+ - `.env.example`
25
+ - `.env`
26
+
27
+ Briefly tell the user which already exist (those will be skipped) and which will be created. Then proceed.
28
+
29
+ - [ ] **Step 1: Detect framework**
30
+
31
+ Read `package.json`. Determine the framework:
32
+ - If `"react"` or `"vite"` in dependencies → framework = `react`, envPrefix = `VITE_`
33
+ - If `"next"` in dependencies → framework = `react`, envPrefix = `NEXT_PUBLIC_`
34
+ - If `"vue"` in dependencies → framework = `react`, envPrefix = `VITE_`; tell the user: "Vue is not yet natively supported — installing React templates instead."
35
+ - Otherwise → framework = `react`, envPrefix = `VITE_`
36
+
37
+ Tell the user: "Detected [framework]. Installing EpicMerch SDK..."
38
+
39
+ - [ ] **Step 2: Install the SDK**
40
+
41
+ Check `package.json` first. If `"epicmerch"` is already in `dependencies`, skip the install and tell the user "epicmerch SDK already installed". Otherwise:
42
+
43
+ Run: `npm install epicmerch`
44
+
45
+ - [ ] **Step 3: Create SDK initializer**
46
+
47
+ **If `src/lib/epicmerch.js` already exists, skip this step entirely** and tell the user "src/lib/epicmerch.js already exists — keeping it". Otherwise, write `src/lib/epicmerch.js` using the `envPrefix` detected in Step 1 to substitute the env var names:
48
+
49
+ - For `VITE_` prefix (Vite / React):
50
+ ```js
51
+ import EpicMerch from 'epicmerch';
52
+
53
+ export const store = new EpicMerch({
54
+ apiKey: import.meta.env.VITE_API_KEY,
55
+ ...(import.meta.env.VITE_API_URL && { baseUrl: import.meta.env.VITE_API_URL }),
56
+ });
57
+ ```
58
+
59
+ - For `NEXT_PUBLIC_` prefix (Next.js):
60
+ ```js
61
+ import EpicMerch from 'epicmerch';
62
+
63
+ export const store = new EpicMerch({
64
+ apiKey: process.env.NEXT_PUBLIC_API_KEY,
65
+ ...(process.env.NEXT_PUBLIC_API_URL && { baseUrl: process.env.NEXT_PUBLIC_API_URL }),
66
+ });
67
+ ```
68
+
69
+ - [ ] **Step 4: Scaffold auth**
70
+
71
+ **If `src/components/Login.jsx` already exists, skip this step** and tell the user "Login.jsx already exists — keeping it". Otherwise write `src/components/Login.jsx`:
72
+
73
+ ```jsx
74
+ import { useState } from 'react';
75
+ import { store } from '../lib/epicmerch';
76
+
77
+ export default function Login({ onLogin }) {
78
+ const [phone, setPhone] = useState('');
79
+ const [otp, setOtp] = useState('');
80
+ const [step, setStep] = useState('phone');
81
+
82
+ const sendOtp = async () => {
83
+ await store.auth.sendOtp(phone, 'phone');
84
+ setStep('otp');
85
+ };
86
+
87
+ const verify = async () => {
88
+ const result = await store.auth.verifyOtp(phone, otp, {});
89
+ if (result.token) {
90
+ store.setCustomerToken(result.token);
91
+ localStorage.setItem('customerInfo', JSON.stringify(result));
92
+ onLogin(result);
93
+ }
94
+ };
95
+
96
+ return (
97
+ <div>
98
+ {step === 'phone' ? (
99
+ <>
100
+ <input value={phone} onChange={e => setPhone(e.target.value)} placeholder="+919876543210" />
101
+ <button onClick={sendOtp}>Send OTP</button>
102
+ </>
103
+ ) : (
104
+ <>
105
+ <input value={otp} onChange={e => setOtp(e.target.value)} placeholder="Enter OTP" />
106
+ <button onClick={verify}>Verify</button>
107
+ </>
108
+ )}
109
+ </div>
110
+ );
111
+ }
112
+ ```
113
+
114
+ - [ ] **Step 5: Scaffold product catalog**
115
+
116
+ For each of the two files in this step, check existence individually. If a file already exists, skip writing it and tell the user "X already exists — keeping it". Otherwise write it.
117
+
118
+ Write `src/components/ProductCard.jsx`:
119
+
120
+ ```jsx
121
+ export default function ProductCard({ product, onAddToCart }) {
122
+ return (
123
+ <div className="product-card">
124
+ <img src={product.images?.[0]} alt={product.name} />
125
+ <h3>{product.name}</h3>
126
+ <p>₹{product.price}</p>
127
+ <button onClick={() => onAddToCart(product._id)}>Add to Cart</button>
128
+ </div>
129
+ );
130
+ }
131
+ ```
132
+
133
+ Write `src/components/ProductList.jsx`:
134
+
135
+ ```jsx
136
+ import { useEffect, useState } from 'react';
137
+ import { store } from '../lib/epicmerch';
138
+ import ProductCard from './ProductCard';
139
+
140
+ export default function ProductList() {
141
+ const [products, setProducts] = useState([]);
142
+ const [categories, setCategories] = useState([]);
143
+ const [category, setCategory] = useState(undefined);
144
+ const [query, setQuery] = useState('');
145
+
146
+ useEffect(() => { store.categories.list().then(setCategories); }, []);
147
+
148
+ useEffect(() => {
149
+ if (query.trim()) {
150
+ store.products.search(query).then(d => setProducts(d.products ?? d));
151
+ } else {
152
+ store.products.list({ type: category, limit: 12 }).then(d => setProducts(d.products ?? d));
153
+ }
154
+ }, [query, category]);
155
+
156
+ const addToCart = (productId) => store.cart.add(productId, 1);
157
+
158
+ return (
159
+ <div>
160
+ <input
161
+ placeholder="Search products..."
162
+ value={query}
163
+ onChange={e => setQuery(e.target.value)}
164
+ />
165
+ <div>
166
+ {categories.map(c => (
167
+ <button key={c} onClick={() => { setQuery(''); setCategory(c === 'All' ? undefined : c); }}>{c}</button>
168
+ ))}
169
+ </div>
170
+ <div className="product-grid">
171
+ {products.map(p => <ProductCard key={p._id} product={p} onAddToCart={addToCart} />)}
172
+ </div>
173
+ </div>
174
+ );
175
+ }
176
+ ```
177
+
178
+ - [ ] **Step 5b: Wire up an existing search bar (only if `ProductList.jsx` was skipped)**
179
+
180
+ If you wrote a fresh `ProductList.jsx` in Step 5, skip this step — the canonical scaffold already has search wired.
181
+
182
+ If `ProductList.jsx` was SKIPPED because it already existed, the merchant has their own UI. They may have a search bar somewhere — in a `Navbar`, a `Header`, a dedicated `SearchBar.jsx`, a search page, etc. — that isn't currently calling EpicMerch's search API. Detect and offer to wire it up.
183
+
184
+ **Detection.** Look through `src/` for files containing any of these signals (case-insensitive):
185
+ - `<input` whose `placeholder` mentions `search`
186
+ - A state variable named `query`, `search`, `searchTerm`, `searchQuery`, or `keyword`
187
+ - A handler named `onSearch`, `handleSearch`, `onQueryChange`, or similar
188
+
189
+ For each match, check whether the file already calls `store.products.search(`. If yes, it's wired — skip silently.
190
+
191
+ **For each unwired match:** show the merchant the file path and the snippet you found, then say:
192
+
193
+ > "I noticed a search input in `<path>` that doesn't call EpicMerch's search yet. The one-line wire-up is:
194
+ >
195
+ > ```js
196
+ > import { store } from '../lib/epicmerch'; // add at top if missing
197
+ >
198
+ > // inside the search handler / useEffect:
199
+ > store.products.search(query).then(d => setProducts(d.products ?? d));
200
+ > ```
201
+ >
202
+ > Want me to apply this to your component? (yes / no — I'll leave it alone if no.)"
203
+
204
+ **WAIT for an explicit yes before editing.** If the merchant declines, move on without changing the file.
205
+
206
+ If you find no search bar anywhere in `src/`, skip this step silently — don't tell the merchant "no search bar found", just continue to Step 6.
207
+
208
+ - [ ] **Step 6: Scaffold cart + orders**
209
+
210
+ For each of the three files in this step (Cart.jsx, Checkout.jsx, OrderHistory.jsx), check existence individually. If a file already exists, skip writing it and tell the user "X already exists — keeping it". Otherwise write it.
211
+
212
+ Write `src/components/Cart.jsx`:
213
+
214
+ ```jsx
215
+ import { useEffect, useState } from 'react';
216
+ import { store } from '../lib/epicmerch';
217
+
218
+ export default function Cart({ onCheckout }) {
219
+ const [cart, setCart] = useState({ items: [] });
220
+
221
+ useEffect(() => { store.cart.get().then(setCart); }, []);
222
+
223
+ const remove = async (productId) => {
224
+ await store.cart.remove(productId);
225
+ store.cart.get().then(setCart);
226
+ };
227
+
228
+ const total = cart.items?.reduce((s, i) => s + i.price * i.qty, 0) ?? 0;
229
+
230
+ return (
231
+ <div>
232
+ {cart.items?.map(item => (
233
+ <div key={item.productId}>
234
+ <span>{item.name} x{item.qty}</span>
235
+ <span>₹{item.price * item.qty}</span>
236
+ <button onClick={() => remove(item.productId)}>Remove</button>
237
+ </div>
238
+ ))}
239
+ <p>Total: ₹{total}</p>
240
+ <button onClick={onCheckout}>Checkout</button>
241
+ </div>
242
+ );
243
+ }
244
+ ```
245
+
246
+ Write `src/components/Checkout.jsx`:
247
+
248
+ ```jsx
249
+ import { useState } from 'react';
250
+ import { store } from '../lib/epicmerch';
251
+
252
+ const loadRazorpay = () =>
253
+ new Promise((resolve) => {
254
+ if (window.Razorpay) return resolve(true);
255
+ const script = document.createElement('script');
256
+ script.src = 'https://checkout.razorpay.com/v1/checkout.js';
257
+ script.onload = () => resolve(true);
258
+ script.onerror = () => resolve(false);
259
+ document.body.appendChild(script);
260
+ });
261
+
262
+ export default function Checkout({ cart, onSuccess }) {
263
+ const [address, setAddress] = useState({ street: '', city: '', postalCode: '', country: 'India' });
264
+
265
+ const placeOrder = async () => {
266
+ await loadRazorpay();
267
+ const total = cart.items.reduce((s, i) => s + i.price * i.qty, 0);
268
+ const { orderId } = await store.orders.create({
269
+ orderItems: cart.items.map(i => ({ productId: i.productId, qty: i.qty, price: i.price })),
270
+ shippingAddress: address,
271
+ paymentMethod: 'Razorpay',
272
+ totalPrice: total,
273
+ });
274
+ const config = await store.payment.getConfig();
275
+ const { razorpayOrderId } = await store.payment.createOrder(total * 100, orderId);
276
+ const rzp = new window.Razorpay({
277
+ key: config.razorpayKeyId,
278
+ order_id: razorpayOrderId,
279
+ amount: total * 100,
280
+ handler: async (response) => {
281
+ await store.payment.verify({ ...response, orderId });
282
+ await store.cart.clear();
283
+ onSuccess(orderId);
284
+ },
285
+ });
286
+ rzp.open();
287
+ };
288
+
289
+ return (
290
+ <div>
291
+ <input placeholder="Street" onChange={e => setAddress(a => ({ ...a, street: e.target.value }))} />
292
+ <input placeholder="City" onChange={e => setAddress(a => ({ ...a, city: e.target.value }))} />
293
+ <input placeholder="Postal Code" onChange={e => setAddress(a => ({ ...a, postalCode: e.target.value }))} />
294
+ <button onClick={placeOrder}>Place Order</button>
295
+ </div>
296
+ );
297
+ }
298
+ ```
299
+
300
+ Do NOT add any Razorpay script tag to `index.html` — the script loads dynamically only when the user clicks Place Order.
301
+
302
+ Write `src/components/OrderHistory.jsx`:
303
+
304
+ ```jsx
305
+ import { useEffect, useState } from 'react';
306
+ import { store } from '../lib/epicmerch';
307
+
308
+ export default function OrderHistory() {
309
+ const [orders, setOrders] = useState([]);
310
+
311
+ useEffect(() => { store.orders.list().then(setOrders); }, []);
312
+
313
+ return (
314
+ <div>
315
+ <h2>Your Orders</h2>
316
+ {orders.map(order => (
317
+ <div key={order._id}>
318
+ <span>Order #{order._id.slice(-6)}</span>
319
+ <span> — ₹{order.totalPrice}</span>
320
+ <span> — {order.status}</span>
321
+ </div>
322
+ ))}
323
+ </div>
324
+ );
325
+ }
326
+ ```
327
+
328
+ - [ ] **Step 7: Generate API key via MCP + write `.env`**
329
+
330
+ This step has two parts: write `.env.example` (the template), then generate a real API key and write a working `.env`.
331
+
332
+ **Part A — `.env.example`:** If `.env.example` already exists, skip writing it and tell the user ".env.example already exists — keeping it". Otherwise write `.env.example`:
333
+
334
+ ```
335
+ VITE_API_KEY=your_epicmerch_api_key_here
336
+ # VITE_API_URL is OPTIONAL — only set it for local development.
337
+ # Production: leave it unset; the SDK defaults to https://api.epicmerch.in/api
338
+ # Local dev: VITE_API_URL=http://localhost:5002/api
339
+ ```
340
+
341
+ Check `.gitignore` — if `.env` is not listed, append `.env` to `.gitignore` before any `.env` file is written.
342
+
343
+ **Part B — generate a real API key + write `.env`:**
344
+
345
+ First, read `.env` if it exists. If it already contains a `VITE_API_KEY=` line whose value is NOT the placeholder string `your_epicmerch_api_key_here` (i.e. there's already a real key), skip Part B entirely and tell the user "Existing API key in .env — keeping it".
346
+
347
+ Otherwise, the merchant is authenticated via the EpicMerch MCP (this skill assumes the MCP is connected — that's how it was invoked). Generate a key by calling the MCP tool:
348
+
349
+ ```
350
+ merchant_generate_api_key({ name: "Storefront" })
351
+ ```
352
+
353
+ The response will include the new key. Extract it (the field is typically `key` or `apiKey` — check the response shape). Then write/update `.env`.
354
+
355
+ **Production (default).** Write only the key — the SDK already knows the prod URL:
356
+
357
+ ```
358
+ VITE_API_KEY=<the generated key from the MCP response>
359
+ ```
360
+
361
+ **Local development.** Only if the merchant is clearly running against a local EpicMerch server (e.g. they explicitly said so, or their existing `.env` already had a `localhost` URL, or the MCP was wired with `EPICMERCH_API_URL=http://localhost:...`), ALSO write the override:
362
+
363
+ ```
364
+ VITE_API_KEY=<the generated key from the MCP response>
365
+ VITE_API_URL=http://localhost:5002/api
366
+ ```
367
+
368
+ When in doubt, default to the production form (no `VITE_API_URL` line). Don't ask the merchant — guess from context.
369
+
370
+ **Fallback if MCP isn't available:** If the `merchant_generate_api_key` tool fails or isn't connected, tell the user: "MCP tool unavailable. Generate a key manually at https://epicmerch.in/dashboard → API Keys and add it to .env." Do NOT fail the whole flow — continue to Step 8.
371
+
372
+ - [ ] **Step 8: Tell the user what to do next**
373
+
374
+ Print a summary using the actual state of the project:
375
+
376
+ - List the files you CREATED in this run (skip the ones that were already present).
377
+ - If you skipped any, briefly mention them as "kept as-is".
378
+ - If Step 5b wired up a search bar in an existing component, mention which file you edited and that it now calls `store.products.search()`.
379
+ - If the API key was generated automatically, mention that ".env now has a working VITE_API_KEY — your storefront should load real product data on next refresh."
380
+ - Then output the next-steps block exactly:
381
+
382
+ ```
383
+ Next steps:
384
+ 1. (API key auto-generated and saved to .env — if not, get one at https://epicmerch.in/dashboard → API Keys)
385
+ 2. Restart your dev server (npm run dev) to pick up the .env changes
386
+ 3. Import the new components into your App.jsx where you need them
387
+
388
+ Need help with payments, analytics, or notifications? Ask me!
389
+ ```