epicmerch-mcp 1.0.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.
@@ -0,0 +1,341 @@
1
+ # EpicMerch SDK Integration Guide for Epic Threadz
2
+
3
+ This document details how the **EpicMerch SDK** is integrated into the **Epic Threadz** storefront. It serves as a reference for developers to understand the interaction between the frontend application and the backend services.
4
+
5
+ ## 1. Installation & Initialization
6
+
7
+ The SDK is imported and initialized in the main application entry point (`src/App.jsx`).
8
+
9
+ ### Initialization
10
+ The `EpicMerch` class is instantiated with the API Key. For local development, the `baseUrl` can be overridden via environment variables.
11
+
12
+ ```javascript
13
+ // src/App.jsx
14
+ import EpicMerch from 'epicmerch';
15
+
16
+ export const store = new EpicMerch({
17
+ apiKey: import.meta.env.VITE_API_KEY || 'your_public_key',
18
+ // Optional: Override for local development
19
+ ...(import.meta.env.VITE_API_URL && { baseUrl: import.meta.env.VITE_API_URL })
20
+ });
21
+ ```
22
+
23
+ The `store` instance is then exported and passed as a prop to child components or imported directly where needed.
24
+
25
+ ---
26
+
27
+ ## 2. Authentication
28
+
29
+ The authentication flow uses OTP (One-Time Password) or Google OAuth. This is handled primarily in `src/pages/Login.jsx`.
30
+
31
+ ### Sending OTP
32
+ ```javascript
33
+ // Send OTP to phone or email
34
+ const response = await store.auth.sendOtp(identifier, 'phone'); // or 'email'
35
+ // identifier: e.g., '+919876543210'
36
+ ```
37
+
38
+ ### Verifying OTP
39
+ ```javascript
40
+ // Verify OTP and complete login/registration
41
+ const result = await store.auth.verifyOtp(identifier, otpValue, {
42
+ name: 'User Name',
43
+ email: 'user@example.com', // optional profile data for new users
44
+ gender: 'Male'
45
+ });
46
+
47
+ // Result contains the authentication token
48
+ if (result.token) {
49
+ store.setCustomerToken(result.token); // Store token in SDK for future requests
50
+ localStorage.setItem('customerInfo', JSON.stringify(result)); // Persist session
51
+ }
52
+ ```
53
+
54
+ ### Google OAuth
55
+ ```javascript
56
+ // Generate OAuth URL
57
+ const authUrl = store.auth.getGoogleAuthUrl(window.location.origin + '/login');
58
+ window.location.href = authUrl;
59
+
60
+ // Handle Callback (on redirect page)
61
+ const result = await store.auth.handleOAuthCallback();
62
+ ```
63
+
64
+ ---
65
+
66
+ ## 3. Product Catalog
67
+
68
+ Product listing and searching are implemented in `src/App.jsx`.
69
+
70
+ ### Fetching Products
71
+ ```javascript
72
+ const params = {
73
+ type: activeCategory, // e.g., 'Apparel' or undefined for all
74
+ page: 1,
75
+ limit: 12,
76
+ sort: 'newest', // 'popularity', 'price_asc', 'price_desc'
77
+ keyword: searchQuery // Optional search term
78
+ };
79
+
80
+ const data = await store.products.list(params);
81
+ // Returns: { products: [...], total, pages }
82
+ ```
83
+
84
+ ---
85
+
86
+ ## 4. Shopping Cart
87
+
88
+ Cart operations are managed in `src/App.jsx` and `src/components/CartSlider.jsx`.
89
+
90
+ ### Core Cart Operations
91
+ ```javascript
92
+ // Get current cart
93
+ const cartData = await store.cart.get();
94
+
95
+ // Add item to cart
96
+ await store.cart.add(productId, quantity, { type: 'Size', value: 'M' });
97
+
98
+ // Update quantity
99
+ await store.cart.update(productId, newQuantity);
100
+
101
+ // Remove item
102
+ await store.cart.remove(productId);
103
+
104
+ // Clear cart
105
+ await store.cart.clear();
106
+ ```
107
+
108
+ ---
109
+
110
+ ## 5. Checkout & Payments
111
+
112
+ The checkout process (`src/pages/Checkout.jsx`) involves address management, order creation, and payment processing via Razorpay.
113
+
114
+ ### Address Management
115
+ ```javascript
116
+ // List saved addresses
117
+ const { addresses } = await store.addresses.list();
118
+
119
+ // Add new address
120
+ await store.addresses.add({
121
+ label: 'Home',
122
+ street: '123 Main St',
123
+ city: 'Mumbai',
124
+ state: 'MH',
125
+ postalCode: '400001',
126
+ country: 'India',
127
+ isDefault: true
128
+ });
129
+ ```
130
+
131
+ ### Order Creation Flow
132
+ 1. **Create Order Record**:
133
+ ```javascript
134
+ const orderResult = await store.orders.create({
135
+ orderItems: [...],
136
+ shippingAddress: {...},
137
+ paymentMethod: 'Razorpay',
138
+ totalPrice: 1500
139
+ });
140
+ const { orderId } = orderResult;
141
+ ```
142
+
143
+ 2. **Initialize Payment**:
144
+ ```javascript
145
+ // Get Razorpay Config
146
+ const config = await store.payment.getConfig();
147
+
148
+ // Create Razorpay Order
149
+ const razorpayOrder = await store.payment.createOrder(amount, orderId);
150
+ ```
151
+
152
+ 3. **Verify Payment**:
153
+ After the Razorpay modal interaction:
154
+ ```javascript
155
+ const verifyResult = await store.payment.verify({
156
+ razorpay_order_id: response.razorpay_order_id,
157
+ razorpay_payment_id: response.razorpay_payment_id,
158
+ razorpay_signature: response.razorpay_signature,
159
+ orderId: orderId
160
+ });
161
+ ```
162
+
163
+ ### Saved Payment Methods
164
+ The SDK supports "One-Click" checkout using saved tokens.
165
+
166
+ ```javascript
167
+ // Get saved cards/UPI
168
+ const methods = await store.payment.getSavedMethods();
169
+
170
+ // Charge a saved method directly
171
+ await store.payment.chargeSaved(methodId, amount, orderId);
172
+
173
+ // Save a new method (after successful payment)
174
+ await store.payment.saveMethod({
175
+ token: `tok_${paymentId}`,
176
+ type: 'card'
177
+ });
178
+ ```
179
+
180
+ ---
181
+
182
+ ## 6. User Profile & Utilities
183
+
184
+ ### Newsletter
185
+ ```javascript
186
+ await store.newsletter.subscribe('user@example.com');
187
+ ```
188
+
189
+ ### Profile Updates
190
+ ```javascript
191
+ // Update phone number or profile info
192
+ await store.customer.updateProfile({ phoneNumber: '+919999988888' });
193
+ ```
194
+
195
+ ---
196
+
197
+ ## 7. Configuration Reference
198
+
199
+ The SDK is configured in `src/App.jsx`.
200
+
201
+ | Option | Description | Default |
202
+ |--------|-------------|---------|
203
+ | `apiKey` | Public API Key for the merchant | Required |
204
+ | `baseUrl`| API Server URL | Auto-detected (or `http://localhost:5001/api`) |
205
+
206
+ ---
207
+
208
+ ## 8. Idempotency (Duplicate Prevention)
209
+
210
+ The SDK automatically handles idempotency for critical operations to prevent duplicate orders and payments.
211
+
212
+ ### How It Works
213
+ - **Order Creation**: SDK generates an idempotency key based on cart contents
214
+ - **Payment Verification**: Uses Razorpay payment ID as idempotency key (guaranteed unique)
215
+ - **Saved Card Charges**: Uses order ID as idempotency key
216
+
217
+ ### Automatic Idempotency (Default Behavior)
218
+ ```javascript
219
+ // The SDK automatically adds Idempotency-Key headers for critical operations
220
+ const order = await store.orders.create(orderData);
221
+ // ↑ Automatically generates key: payment_${orderId}
222
+
223
+ await store.payment.verify(paymentData);
224
+ // ↑ Automatically uses: verify_${razorpay_payment_id}
225
+ ```
226
+
227
+ ### Custom Idempotency Keys
228
+ For checkout flows, provide a stable key to prevent double-click issues:
229
+
230
+ ```javascript
231
+ // Generate a stable key once per checkout session
232
+ const checkoutKey = `checkout_${userId}_${Date.now()}`;
233
+
234
+ // Use the same key for the entire checkout
235
+ const order = await store.orders.create(orderData, {
236
+ idempotencyKey: checkoutKey
237
+ });
238
+ ```
239
+
240
+ ### Idempotent Response Detection
241
+ ```javascript
242
+ const result = await store.orders.create(orderData, { idempotencyKey: 'my-key' });
243
+
244
+ if (result._idempotent) {
245
+ // This is a cached response from a previous identical request
246
+ console.log('Order already created:', result.orderId);
247
+ }
248
+ ```
249
+
250
+ ---
251
+
252
+ ## 9. Transactional APIs
253
+
254
+ Critical operations (order creation, payment verification) are transactional:
255
+
256
+ ### Order Creation
257
+ - ✅ Create order record
258
+ - ✅ Reduce product stock
259
+ - ✅ Update user (add order, clear cart)
260
+ - ❌ If ANY step fails, ALL changes are rolled back
261
+
262
+ ### Payment Verification
263
+ - ✅ Validates Razorpay signature
264
+ - ✅ Atomically updates order status
265
+ - ✅ Prevents double payment crediting
266
+ - ✅ Returns `_alreadyVerified: true` for duplicate requests
267
+
268
+ ---
269
+
270
+ ## 10. Error Handling Best Practices
271
+
272
+ ```javascript
273
+ try {
274
+ const order = await store.orders.create(orderData);
275
+
276
+ if (order._idempotent) {
277
+ // Handle duplicate request gracefully
278
+ showSuccessMessage('Order already placed!');
279
+ return;
280
+ }
281
+
282
+ // Proceed with payment
283
+ const razorpay = await store.payment.createOrder(amount, order.orderId);
284
+
285
+ } catch (error) {
286
+ if (error.response?.status === 409) {
287
+ // Conflict - request is being processed
288
+ showMessage('Your request is being processed. Please wait...');
289
+ } else if (error.response?.status === 429) {
290
+ // Rate limited
291
+ showError('Too many requests. Please try again later.');
292
+ } else {
293
+ showError(error.message || 'Something went wrong');
294
+ }
295
+ }
296
+ ```
297
+
298
+ ---
299
+
300
+ ## 11. Checkout Page Best Practices
301
+
302
+ ### Editable Cart on Checkout
303
+ The checkout page now supports inline cart editing:
304
+
305
+ ```javascript
306
+ // Update quantity
307
+ const handleUpdateQty = async (productId, newQty, variant) => {
308
+ await store.cart.update(productId, newQty, variant);
309
+ // Refresh cart state
310
+ const cartData = await store.cart.get();
311
+ setCart(cartData.cart);
312
+ };
313
+
314
+ // Remove item
315
+ const handleRemoveItem = async (productId, variant) => {
316
+ await store.cart.remove(productId, variant);
317
+ // Refresh cart state
318
+ const cartData = await store.cart.get();
319
+ setCart(cartData.cart);
320
+ };
321
+ ```
322
+
323
+ ### Success Modal Pattern
324
+ Replace browser alerts with styled modals:
325
+
326
+ ```javascript
327
+ const [successModal, setSuccessModal] = useState({
328
+ show: false,
329
+ trackingNumber: ''
330
+ });
331
+
332
+ // After payment verification
333
+ setSuccessModal({
334
+ show: true,
335
+ trackingNumber: orderResult.trackingNumber
336
+ });
337
+ ```
338
+
339
+ ---
340
+
341
+ **Note**: All SDK methods return Promises. Use `async/await` and appropriate `try/catch` blocks for error handling.
package/src/status.js ADDED
@@ -0,0 +1,22 @@
1
+ // src/status.js
2
+ import { readTokenStore } from './token-store.js';
3
+
4
+ export async function status() {
5
+ const data = readTokenStore();
6
+ if (!data || Object.keys(data.stores).length === 0) {
7
+ console.error('Not logged in. Run `npx epicmerch-mcp login` to set up OAuth.');
8
+ return;
9
+ }
10
+ console.error(`Default store: ${data.defaultStore}`);
11
+ for (const [name, entry] of Object.entries(data.stores)) {
12
+ const accessExp = new Date(entry.accessTokenExpiresAt);
13
+ const refreshExp = new Date(entry.refreshTokenExpiresAt);
14
+ const now = new Date();
15
+ const accessStatus = accessExp > now ? `valid until ${accessExp.toISOString()}` : 'EXPIRED (will refresh)';
16
+ const refreshStatus = refreshExp > now ? `valid until ${refreshExp.toISOString()}` : 'EXPIRED — run `epicmerch-mcp login` again';
17
+ console.error(`\n ${name}${name === data.defaultStore ? ' (default)' : ''}`);
18
+ console.error(` Access token: ${accessStatus}`);
19
+ console.error(` Refresh token: ${refreshStatus}`);
20
+ console.error(` Scope: ${entry.scope}`);
21
+ }
22
+ }
@@ -0,0 +1,52 @@
1
+ // src/token-store.js
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { join, dirname } from 'path';
5
+
6
+ export function tokenPath() {
7
+ return join(homedir(), '.epicmerch', 'token.json');
8
+ }
9
+
10
+ export function readTokenStore() {
11
+ const p = tokenPath();
12
+ if (!existsSync(p)) return null;
13
+ let raw;
14
+ try { raw = readFileSync(p, 'utf-8'); }
15
+ catch (e) { throw new Error(`Could not read token file ${p}: ${e.message}`); }
16
+ try { return JSON.parse(raw); }
17
+ catch (e) { throw new Error(`Token file ${p} is corrupt JSON: ${e.message}. Delete it and run 'npx epicmerch-mcp login' again.`); }
18
+ }
19
+
20
+ export function writeTokenStore(data) {
21
+ const p = tokenPath();
22
+ const dir = dirname(p);
23
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
24
+ writeFileSync(p, JSON.stringify(data, null, 2) + '\n');
25
+ try { chmodSync(p, 0o600); } catch (_) { /* Windows no-op */ }
26
+ }
27
+
28
+ export function upsertStore(name, entry) {
29
+ const data = readTokenStore() ?? { version: 1, defaultStore: null, stores: {} };
30
+ data.stores[name] = entry;
31
+ if (!data.defaultStore) data.defaultStore = name;
32
+ writeTokenStore(data);
33
+ }
34
+
35
+ export function removeStore(name) {
36
+ const data = readTokenStore();
37
+ if (!data) return;
38
+ delete data.stores[name];
39
+ if (data.defaultStore === name) {
40
+ const remaining = Object.keys(data.stores);
41
+ data.defaultStore = remaining[0] ?? null;
42
+ }
43
+ writeTokenStore(data);
44
+ }
45
+
46
+ export function getActiveEntry() {
47
+ const data = readTokenStore();
48
+ if (!data) return null;
49
+ const targetName = process.env.EPICMERCH_ACTIVE_STORE || data.defaultStore;
50
+ if (!targetName) return null;
51
+ return data.stores[targetName] ?? null;
52
+ }