@brainerce/mcp-server 3.1.0 → 3.2.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/dist/bin/http.js +534 -53
- package/dist/bin/stdio.js +534 -53
- package/dist/index.js +534 -53
- package/dist/index.mjs +534 -53
- package/package.json +53 -53
package/dist/index.js
CHANGED
|
@@ -91,7 +91,7 @@ function getTypeQuickReference() {
|
|
|
91
91
|
| OAuth | \`provider.name\` | \`provider\` | it IS the string: \`'GOOGLE' | 'FACEBOOK' | 'GITHUB'\` |
|
|
92
92
|
| OAuth | \`oauth.url\` | \`oauth.authorizationUrl\` | |
|
|
93
93
|
| Description | \`getDescriptionContent()\` | \`{ html } | { text } | null\` | check \`'html' in content\` before use |
|
|
94
|
-
| \`smartGetCart()\` |
|
|
94
|
+
| \`smartGetCart()\` | N/A | \`cart.id\` | returns \`CartWithIncludes\` (extends \`Cart\`); pass \`{ include: [...] }\` for extras |
|
|
95
95
|
| \`startGuestCheckout()\` | always use \`.checkoutId\` | check \`result.tracked\` | discriminated union |
|
|
96
96
|
|
|
97
97
|
**Rules:**
|
|
@@ -99,8 +99,8 @@ function getTypeQuickReference() {
|
|
|
99
99
|
- CartItem/CheckoutLineItem = **NESTED** (\`item.product.name\`, \`item.unitPrice\`)
|
|
100
100
|
- OrderItem = **FLAT** (\`item.name\`, \`item.price\`, \`item.totalPrice\`, \`item.image\`)
|
|
101
101
|
- Cart has NO \`.total\` \u2014 use \`getCartTotals(cart)\`
|
|
102
|
-
- \`getCartTotals()\`
|
|
103
|
-
- \`smartGetCart()\` returns \`
|
|
102
|
+
- \`getCartTotals()\` works with server \`Cart\` (all carts are server-side now)
|
|
103
|
+
- \`smartGetCart()\` returns \`CartWithIncludes\` \u2014 always a server cart; pass \`{ include: ['recommendations', 'upgrades', 'bundles'] }\` for extras
|
|
104
104
|
- \`startGuestCheckout()\` returns discriminated union \u2014 check \`result.tracked\` before \`.checkoutId\`
|
|
105
105
|
- \`SetShippingAddressDto.email\` is **required** \u2014 always include email
|
|
106
106
|
|
|
@@ -228,7 +228,7 @@ async function startCheckout() {
|
|
|
228
228
|
6. Select shipping method \u2192 \`selectShippingMethod()\`
|
|
229
229
|
6b. (Optional) Set checkout custom fields \u2192 \`setCheckoutCustomFields()\` \u2014 surcharges auto-calculated
|
|
230
230
|
7. Create payment intent \u2192 \`createPaymentIntent()\` \u2192 returns \`{ clientSecret, provider }\`
|
|
231
|
-
8. Branch on \`provider
|
|
231
|
+
8. Branch on the **\`clientSdk.renderType\`** returned by \`createPaymentIntent()\` \u2014 NEVER hard-code by provider name. Values: \`'sdk-widget'\` (Stripe, PayPal, Grow), \`'iframe'\` (Cardcom hosted or Brainerce-hosted embed \u2014 detect via URL path), \`'redirect'\`, \`'sandbox'\`, \`'embedded-fields'\` (reserved).
|
|
232
232
|
9. **Order is created AUTOMATICALLY after payment succeeds (via webhook) \u2014 for ALL providers!**
|
|
233
233
|
|
|
234
234
|
### Checkout Page Component
|
|
@@ -331,8 +331,8 @@ function CheckoutPage() {
|
|
|
331
331
|
</div>
|
|
332
332
|
);
|
|
333
333
|
}
|
|
334
|
-
if (paymentData.
|
|
335
|
-
return <
|
|
334
|
+
if (paymentData.clientSdk?.renderType === 'iframe') {
|
|
335
|
+
return <PaymentIframe clientSecret={paymentData.clientSecret} checkoutId={paymentData.checkoutId} />;
|
|
336
336
|
}
|
|
337
337
|
if (paymentData.provider === 'paypal' && paypalClientId) {
|
|
338
338
|
return <PayPalPaymentForm clientId={paypalClientId} orderId={paymentData.clientSecret} checkoutId={paymentData.checkoutId} />;
|
|
@@ -388,26 +388,82 @@ function StripePaymentForm({ checkoutId }: { checkoutId: string }) {
|
|
|
388
388
|
}
|
|
389
389
|
\`\`\`
|
|
390
390
|
|
|
391
|
-
###
|
|
391
|
+
### Iframe-Based Providers (Cardcom, legacy Grow, etc.)
|
|
392
|
+
|
|
393
|
+
When \`clientSdk.renderType === 'iframe'\`, the payment intent returns a \`clientSecret\` which is a URL to load in an iframe. There are TWO flavors of iframe rendering you must handle:
|
|
394
|
+
|
|
395
|
+
**Flavor A \u2014 Brainerce-hosted embed (URL path contains \`/embed/\`):** The iframe loads a Brainerce-branded compact form (e.g. Cardcom OpenFields). Render it **INLINE** inside your checkout flow (next to the order summary) \u2014 NO modal, NO dark overlay. The embed page posts messages to resize itself and to request top-level navigation (e.g. for Bit express-pay buttons).
|
|
396
|
+
|
|
397
|
+
**Flavor B \u2014 Provider-hosted page (any other URL):** The iframe loads a full provider-branded page with its own header/chrome. Render inside a **modal overlay** so it doesn't fight your checkout layout.
|
|
398
|
+
|
|
399
|
+
Detect flavor by URL path \u2014 works across localhost/staging/prod without a domain list:
|
|
392
400
|
|
|
393
401
|
\`\`\`typescript
|
|
394
|
-
function
|
|
395
|
-
const [
|
|
402
|
+
function PaymentIframe({ clientSecret, checkoutId }: { clientSecret: string; checkoutId: string }) {
|
|
403
|
+
const [height, setHeight] = useState(540); // default before resize message arrives
|
|
404
|
+
const isBrainerceEmbed = (() => {
|
|
405
|
+
try { return new URL(clientSecret).pathname.includes('/embed/'); }
|
|
406
|
+
catch { return false; }
|
|
407
|
+
})();
|
|
408
|
+
|
|
409
|
+
useEffect(() => {
|
|
410
|
+
function handleMessage(e: MessageEvent) {
|
|
411
|
+
const data = e.data as { type?: string; height?: number; url?: string };
|
|
412
|
+
if (data?.type === 'brainerce:resize' && typeof data.height === 'number') {
|
|
413
|
+
setHeight(data.height);
|
|
414
|
+
}
|
|
415
|
+
if (data?.type === 'brainerce:redirect' && typeof data.url === 'string') {
|
|
416
|
+
// Top-level navigation (e.g. Bit). ALWAYS validate against an allowlist
|
|
417
|
+
// before navigating \u2014 never trust the URL blindly.
|
|
418
|
+
if (isTrustedPaymentUrl(data.url)) { window.top!.location.href = data.url; }
|
|
419
|
+
}
|
|
420
|
+
if (data?.type === 'brainerce:payment-complete') {
|
|
421
|
+
// Payment done \u2014 redirect to confirmation page which verifies server-side
|
|
422
|
+
window.location.href = \`/order-confirmation?checkout_id=\${checkoutId}\`;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
window.addEventListener('message', handleMessage);
|
|
426
|
+
return () => window.removeEventListener('message', handleMessage);
|
|
427
|
+
}, [checkoutId]);
|
|
428
|
+
|
|
429
|
+
if (isBrainerceEmbed) {
|
|
430
|
+
// Inline: part of the checkout flow, no overlay
|
|
431
|
+
return (
|
|
432
|
+
<div className="w-full">
|
|
433
|
+
<iframe
|
|
434
|
+
src={clientSecret}
|
|
435
|
+
style={{ width: '100%', height, border: 0, transition: 'height 0.2s ease-out' }}
|
|
436
|
+
title="Payment"
|
|
437
|
+
allow="payment"
|
|
438
|
+
/>
|
|
439
|
+
</div>
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Provider-hosted page: modal overlay
|
|
396
444
|
return (
|
|
397
|
-
<div className="
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
Having trouble? <a href={paymentUrl} target="_blank" rel="noopener noreferrer" className="text-blue-600 underline">Open payment in new tab</a>
|
|
407
|
-
</p>
|
|
445
|
+
<div className="fixed inset-0 z-50 flex items-start justify-center bg-black/50 py-6 overflow-y-auto">
|
|
446
|
+
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-4xl mx-4">
|
|
447
|
+
<iframe
|
|
448
|
+
src={clientSecret}
|
|
449
|
+
style={{ width: '100%', height: '90vh', minHeight: 700, border: 0 }}
|
|
450
|
+
title="Payment"
|
|
451
|
+
allow="payment"
|
|
452
|
+
/>
|
|
453
|
+
</div>
|
|
408
454
|
</div>
|
|
409
455
|
);
|
|
410
456
|
}
|
|
457
|
+
|
|
458
|
+
// Allowlist of origins you trust for top-level payment redirects.
|
|
459
|
+
function isTrustedPaymentUrl(url: string): boolean {
|
|
460
|
+
try {
|
|
461
|
+
const u = new URL(url);
|
|
462
|
+
if (u.protocol !== 'https:') return false;
|
|
463
|
+
// Add your provider hostnames here. Example: Cardcom for Bit express-pay.
|
|
464
|
+
return u.hostname === 'cardcom.solutions' || u.hostname.endsWith('.cardcom.solutions');
|
|
465
|
+
} catch { return false; }
|
|
466
|
+
}
|
|
411
467
|
\`\`\`
|
|
412
468
|
|
|
413
469
|
### PayPal Payment Form
|
|
@@ -677,12 +733,23 @@ const growProvider = providers.find(p => p.provider === 'grow');
|
|
|
677
733
|
const paypalProvider = providers.find(p => p.provider === 'paypal');
|
|
678
734
|
\`\`\`
|
|
679
735
|
|
|
680
|
-
Each provider has: \`id\`, \`provider\` ('stripe'
|
|
736
|
+
Each provider has: \`id\`, \`provider\` (flexible string \u2014 \`'stripe'\`, \`'grow'\`, \`'paypal'\`, \`'cardcom'\`, \`'sandbox'\`, and future providers), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
|
|
737
|
+
|
|
738
|
+
The payment intent returned from \`createPaymentIntent()\` includes a \`clientSdk\` object whose \`renderType\` tells you exactly how to render \u2014 **branch on THAT, not on the provider name**:
|
|
681
739
|
|
|
740
|
+
- \`renderType: 'sdk-widget'\` \u2014 load \`clientSdk.scriptUrl\`, mount into \`<div id={clientSdk.containerId}>\`. Used by Stripe, PayPal, Grow. The SDK paints its own form in your DOM.
|
|
741
|
+
- \`renderType: 'iframe'\` \u2014 render \`<iframe src={clientSecret}>\`. Two flavors detected by URL path:
|
|
742
|
+
- Path contains \`/embed/\` \u2192 Brainerce-hosted embed. Render **INLINE** (no modal). Listen for \`brainerce:resize\` / \`brainerce:redirect\` postMessages. Used by Cardcom (embedded mode).
|
|
743
|
+
- Any other URL \u2192 provider-hosted page. Render inside a **modal**. Used by Cardcom (hosted mode), legacy Grow iframe.
|
|
744
|
+
- \`renderType: 'redirect'\` \u2014 \`window.location.href = URL\`. Customer completes payment off-site and returns via SuccessRedirectUrl.
|
|
745
|
+
- \`renderType: 'sandbox'\` \u2014 show a "Complete Test Order" button; call \`completeGuestCheckout(checkoutId)\`. Orders are \`isTestOrder: true\`. Appears when \`sandboxPaymentsEnabled\` is true.
|
|
746
|
+
- \`renderType: 'embedded-fields'\` \u2014 reserved for future Stripe-Elements-style pattern (PCI micro-iframes for card/CVV mounted directly into the merchant form). **No provider ships this today** \u2014 handle via a "not supported yet" fallback.
|
|
747
|
+
|
|
748
|
+
Provider-specific install notes:
|
|
682
749
|
- **Stripe:** \`npm install @stripe/stripe-js @stripe/react-stripe-js\` \u2014 \`loadStripe(publicKey, { stripeAccount })\`
|
|
683
|
-
- **Grow:** No SDK \u2014 uses iframe with payment URL. Supports credit cards, Bit, Apple Pay, Google Pay.
|
|
684
750
|
- **PayPal:** \`npm install @paypal/react-paypal-js\` \u2014 \`PayPalScriptProvider\` + \`PayPalButtons\`
|
|
685
|
-
- **
|
|
751
|
+
- **Grow:** No SDK needed \u2014 JS SDK loaded via \`clientSdk.scriptUrl\`. Supports credit cards, Bit, Apple Pay, Google Pay.
|
|
752
|
+
- **Cardcom:** No SDK needed \u2014 rendering driven by \`renderType: 'iframe'\` + the inline/modal branch above. Supports credit cards, Bit (when terminal provisions it), installments, 3D Secure.`;
|
|
686
753
|
}
|
|
687
754
|
function getProductsSection(_currency) {
|
|
688
755
|
return `## Products & Variants
|
|
@@ -713,10 +780,12 @@ When \`storeInfo.i18n.enabled\` is true, call \`client.setLocale(locale)\` once
|
|
|
713
780
|
\`{ locale }\` params needed. Translated fields include: \`name\`,
|
|
714
781
|
\`description\`, \`slug\`, \`seoTitle\`, \`seoDescription\`, plus
|
|
715
782
|
\`categories[].name\`, \`brands[].name\`, \`tags[].name\`, \`variants[].name\`,
|
|
716
|
-
\`
|
|
717
|
-
\`
|
|
718
|
-
|
|
719
|
-
and
|
|
783
|
+
\`variant.attributes\` keys and values (so \`getVariantOptions()\` returns
|
|
784
|
+
translated attribute/option names), \`productAttributeOptions[].attribute.name\`/
|
|
785
|
+
\`attributeOption.name\`, \`metafields[].value\`, cart item product/variant names,
|
|
786
|
+
and recommendation/bundle/bump product names. No client-side overlay. See the
|
|
787
|
+
full \`i18n\` section (\`get-sdk-docs\` topic \`i18n\`) for the \`[locale]\` route
|
|
788
|
+
pattern, SDK setup, and the brand/tag badge examples.
|
|
720
789
|
|
|
721
790
|
### Price Display (Use SDK Helper!)
|
|
722
791
|
|
|
@@ -853,6 +922,8 @@ const material = getProductMetafieldValue(product, 'material');
|
|
|
853
922
|
|
|
854
923
|
Some products allow customers to provide input at purchase time (e.g., text on a cake, upload a logo). Check \`product.customizationFields\` and render input fields accordingly.
|
|
855
924
|
|
|
925
|
+
Merchants can also flag a \`MetafieldDefinition\` as \`appliesToAllProducts: true\` \u2014 those fields are folded into every product's \`customizationFields\` array automatically by the backend. Client code does not need to merge or union anything: always render exactly what's in \`product.customizationFields\`.
|
|
926
|
+
|
|
856
927
|
\`\`\`typescript
|
|
857
928
|
import { getProductCustomizationFields } from 'brainerce';
|
|
858
929
|
import type { ProductCustomizationField } from 'brainerce';
|
|
@@ -911,7 +982,7 @@ function getCartSection(_currency) {
|
|
|
911
982
|
### Smart Cart Methods (RECOMMENDED)
|
|
912
983
|
|
|
913
984
|
\`\`\`typescript
|
|
914
|
-
const cart = await client.smartGetCart(); // Returns
|
|
985
|
+
const cart = await client.smartGetCart(); // Returns CartWithIncludes (extends Cart)
|
|
915
986
|
await client.smartAddToCart({
|
|
916
987
|
productId: product.id,
|
|
917
988
|
variantId: selectedVariant?.id,
|
|
@@ -1042,7 +1113,19 @@ const recs = (product as any).recommendations as ProductRecommendationsResponse
|
|
|
1042
1113
|
|
|
1043
1114
|
### Cart Page Features
|
|
1044
1115
|
|
|
1045
|
-
**
|
|
1116
|
+
**Consolidated include** (fetch recommendations, upgrades, and bundles in one request):
|
|
1117
|
+
\`\`\`typescript
|
|
1118
|
+
import type { CartWithIncludes } from 'brainerce';
|
|
1119
|
+
const cart = await client.getCart(cartId, {
|
|
1120
|
+
include: ['recommendations', 'upgrades', 'bundles'],
|
|
1121
|
+
});
|
|
1122
|
+
// cart.recommendations \u2014 cross-sell recommendations
|
|
1123
|
+
// cart.upgrades \u2014 upgrade suggestions keyed by source product ID
|
|
1124
|
+
// cart.bundles \u2014 bundle offers
|
|
1125
|
+
// Also works with smartGetCart: client.smartGetCart({ include: ['recommendations', 'upgrades', 'bundles'] })
|
|
1126
|
+
\`\`\`
|
|
1127
|
+
|
|
1128
|
+
**Cross-sell recommendations** (or fetch individually for targeted refresh):
|
|
1046
1129
|
\`\`\`typescript
|
|
1047
1130
|
import type { CartRecommendationsResponse } from 'brainerce';
|
|
1048
1131
|
const cartRecs = await client.getCartRecommendations(cartId, 4);
|
|
@@ -1134,6 +1217,110 @@ Always check the flag before rendering: \`if (storeInfo?.upsell?.featureName !==
|
|
|
1134
1217
|
ProductRecommendation: \`id\`, \`name\`, \`slug\`, \`basePrice\`, \`salePrice\`, \`images\`, \`type\`, \`inventory\`, \`relationType\`, \`variants?\` (when variant selection needed).
|
|
1135
1218
|
OrderBump/CartBundleOffer: includes \`requiresVariantSelection\` (boolean) and \`lockedVariant?\` (\`{ id, name, attributes }\`).`;
|
|
1136
1219
|
}
|
|
1220
|
+
function getProductCustomizationFieldsSection() {
|
|
1221
|
+
return `## Product Customization Fields (buyer input on product page)
|
|
1222
|
+
|
|
1223
|
+
Some products let the buyer personalize them before adding to cart \u2014 engraving text, a photo upload, a color pick, a multi-select add-on list. The merchant defines these fields in the Brainerce dashboard; the storefront renders them, collects values, uploads any images, and passes everything as \`metadata\` on add-to-cart.
|
|
1224
|
+
|
|
1225
|
+
### Where the fields come from
|
|
1226
|
+
|
|
1227
|
+
They arrive **embedded on the product response** \u2014 no extra API call:
|
|
1228
|
+
|
|
1229
|
+
\`\`\`typescript
|
|
1230
|
+
import type { Product, ProductCustomizationField, MetafieldType } from 'brainerce';
|
|
1231
|
+
|
|
1232
|
+
const product = await client.getProductBySlug(slug);
|
|
1233
|
+
const fields: ProductCustomizationField[] = product.customizationFields ?? [];
|
|
1234
|
+
// fields is sorted by position. If empty, render the product page normally.
|
|
1235
|
+
\`\`\`
|
|
1236
|
+
|
|
1237
|
+
Each field has:
|
|
1238
|
+
\`\`\`typescript
|
|
1239
|
+
{
|
|
1240
|
+
definitionId: string;
|
|
1241
|
+
key: string; // stable identifier \u2014 use this as the metadata key
|
|
1242
|
+
name: string; // display label (may be localized)
|
|
1243
|
+
description?: string | null;
|
|
1244
|
+
type: MetafieldType; // 'TEXT' | 'TEXTAREA' | 'NUMBER' | 'BOOLEAN' | 'DATE' | 'DATETIME' |
|
|
1245
|
+
// 'JSON' | 'URL' | 'COLOR' | 'DIMENSION' | 'WEIGHT' |
|
|
1246
|
+
// 'IMAGE' | 'GALLERY' | 'SELECT' | 'MULTI_SELECT'
|
|
1247
|
+
required: boolean;
|
|
1248
|
+
minLength?: number | null; // chars for TEXT/TEXTAREA; array size for MULTI_SELECT
|
|
1249
|
+
maxLength?: number | null;
|
|
1250
|
+
minValue?: number | null; // NUMBER / DIMENSION / WEIGHT
|
|
1251
|
+
maxValue?: number | null;
|
|
1252
|
+
enumValues?: string[]; // required for SELECT / MULTI_SELECT
|
|
1253
|
+
defaultValue?: string | null;
|
|
1254
|
+
position: number;
|
|
1255
|
+
}
|
|
1256
|
+
\`\`\`
|
|
1257
|
+
|
|
1258
|
+
### Render one control per type
|
|
1259
|
+
|
|
1260
|
+
| type | Render as | Collected value |
|
|
1261
|
+
| --------------- | ---------------------------------------------------------- | ---------------------------- |
|
|
1262
|
+
| \`TEXT\` | \`<input type="text">\` | \`string\` |
|
|
1263
|
+
| \`TEXTAREA\` | \`<textarea>\` | \`string\` |
|
|
1264
|
+
| \`NUMBER\` | \`<input type="number">\` | \`number\` |
|
|
1265
|
+
| \`BOOLEAN\` | checkbox / switch | \`boolean\` |
|
|
1266
|
+
| \`DATE\` | \`<input type="date">\` | \`string\` (YYYY-MM-DD) |
|
|
1267
|
+
| \`DATETIME\` | \`<input type="datetime-local">\` | \`string\` (ISO 8601) |
|
|
1268
|
+
| \`URL\` | \`<input type="url">\` | \`string\` |
|
|
1269
|
+
| \`COLOR\` | \`<input type="color">\` | \`string\` (#RRGGBB) |
|
|
1270
|
+
| \`SELECT\` | \`<select>\` or radio group (use \`enumValues\`) | \`string\` (in enumValues) |
|
|
1271
|
+
| \`MULTI_SELECT\`| checkbox group (use \`enumValues\`) | \`string[]\` (every in enum) |
|
|
1272
|
+
| \`IMAGE\` | file input + upload via \`uploadCustomizationFile\` | \`string\` (URL) |
|
|
1273
|
+
| \`GALLERY\` | multi-file input + one upload per file | \`string[]\` (URLs) |
|
|
1274
|
+
| \`JSON\` | textarea; validate is parseable | \`string\` (JSON) |
|
|
1275
|
+
|
|
1276
|
+
### Upload buyer-submitted images
|
|
1277
|
+
|
|
1278
|
+
\`IMAGE\` and \`GALLERY\` require uploading before add-to-cart:
|
|
1279
|
+
|
|
1280
|
+
\`\`\`typescript
|
|
1281
|
+
const { url } = await client.uploadCustomizationFile(file);
|
|
1282
|
+
// Server rules: image/* only, max 5 MB, 10 uploads/min per IP.
|
|
1283
|
+
// Files are retained for at least 7 days; after that, if the cart never
|
|
1284
|
+
// became an order, they are automatically deleted.
|
|
1285
|
+
\`\`\`
|
|
1286
|
+
|
|
1287
|
+
### Add to cart with customization metadata
|
|
1288
|
+
|
|
1289
|
+
\`\`\`typescript
|
|
1290
|
+
// Collect values keyed by field.key \u2014 NOT by field.name or field.definitionId.
|
|
1291
|
+
const metadata: Record<string, unknown> = {
|
|
1292
|
+
engraving_text: 'Happy Birthday!', // TEXT
|
|
1293
|
+
frame_color: 'Gold', // SELECT (must be in enumValues)
|
|
1294
|
+
upload_photo: url, // IMAGE (URL from uploadCustomizationFile)
|
|
1295
|
+
addons: ['Gift wrap'], // MULTI_SELECT (always an array)
|
|
1296
|
+
};
|
|
1297
|
+
|
|
1298
|
+
await client.addToCart(cart.id, {
|
|
1299
|
+
productId: product.id,
|
|
1300
|
+
quantity: 1,
|
|
1301
|
+
metadata,
|
|
1302
|
+
});
|
|
1303
|
+
\`\`\`
|
|
1304
|
+
|
|
1305
|
+
Server validation (rejected with HTTP 400 on failure):
|
|
1306
|
+
- \`required: true\` \u2192 must be present and non-empty
|
|
1307
|
+
- \`TEXT\` / \`TEXTAREA\` \u2192 string; \`minLength\` / \`maxLength\` enforced as character count
|
|
1308
|
+
- \`NUMBER\` \u2192 \`minValue\` / \`maxValue\` enforced
|
|
1309
|
+
- \`SELECT\` \u2192 value must be one of \`enumValues\`
|
|
1310
|
+
- \`MULTI_SELECT\` \u2192 array; each element in \`enumValues\`; duplicates removed; \`minLength\` / \`maxLength\` enforced as array size
|
|
1311
|
+
- \`IMAGE\` / \`GALLERY\` \u2192 URL(s) must be from \`/customization-upload\` on this store
|
|
1312
|
+
|
|
1313
|
+
### Order snapshot
|
|
1314
|
+
|
|
1315
|
+
When the order is created, each line's customization values are snapshotted onto the order item with \`{ key, name, type, value }\` \u2014 so later edits to the field definition don't orphan old orders. The merchant dashboard shows the values on the order detail page.
|
|
1316
|
+
|
|
1317
|
+
### Common mistakes
|
|
1318
|
+
|
|
1319
|
+
- Passing values keyed by \`field.name\` instead of \`field.key\` \u2192 silently ignored (unknown key)
|
|
1320
|
+
- Sending a \`MULTI_SELECT\` as a string instead of \`string[]\` \u2192 HTTP 400
|
|
1321
|
+
- Using a raw external URL (not from \`/customization-upload\`) for \`IMAGE\` / \`GALLERY\` \u2192 HTTP 400
|
|
1322
|
+
- Assuming \`enumValues\` is present for all types \u2014 only \`SELECT\` / \`MULTI_SELECT\` require it`;
|
|
1323
|
+
}
|
|
1137
1324
|
function getInventorySection() {
|
|
1138
1325
|
return `## Inventory, Stock Display & Reservation Countdown
|
|
1139
1326
|
|
|
@@ -1390,7 +1577,50 @@ const { data: orders } = await client.getMyOrders({ page: 1, limit: 10 }); // Re
|
|
|
1390
1577
|
// \u274C WRONG \u2014 these methods don't exist!
|
|
1391
1578
|
client.getCustomerProfile();
|
|
1392
1579
|
client.getCustomerOrders();
|
|
1393
|
-
|
|
1580
|
+
\`\`\`
|
|
1581
|
+
|
|
1582
|
+
### Order history should show more than just totals
|
|
1583
|
+
|
|
1584
|
+
A useful order card includes ALL of the following when the data is present. Each section is conditional \u2014 render nothing if the field is empty.
|
|
1585
|
+
|
|
1586
|
+
| Section | Source field | Notes |
|
|
1587
|
+
|---|---|---|
|
|
1588
|
+
| Header (number, status badge, date, total) | \`order.orderNumber\`, \`order.status\`, \`order.createdAt\`, \`order.totalAmount\` | Always present. |
|
|
1589
|
+
| Line items | \`order.items[]\` | Render image, name, qty, price. |
|
|
1590
|
+
| **Per-item customizations** | \`order.items[i].customizations\` | Map of { label, value, type }. Render by \`type\` \u2014 see table below. |
|
|
1591
|
+
| **Status timeline** | \`order.statusHistory\` | \`OrderStatusChange[]\`: \`{ status, at, note? }\`. Render as a vertical list. |
|
|
1592
|
+
| **Shipping address** | \`order.shippingAddress\` | Standard \`OrderAddress\` shape. |
|
|
1593
|
+
| **Tracking** | \`order.trackingNumber\`, \`order.trackingUrl\`, \`order.carrier\`, \`order.shippedAt\`, \`order.deliveredAt\` | Link out to \`trackingUrl\` when set. |
|
|
1594
|
+
| **Payment** | \`order.paymentMethod\`, \`order.financialStatus\` | Badge \`financialStatus\` (paid / pending / refunded / partially_refunded). |
|
|
1595
|
+
| Downloads | \`order.hasDownloads\` \u2192 \`client.getOrderDownloads(id)\` | Separate call; returns \`OrderDownloadLink[]\`. |
|
|
1596
|
+
| Financial summary | \`order.subtotal\`, \`order.appliedDiscounts\`, \`order.couponCode\` + \`couponDiscount\`, \`order.shippingAmount\`, \`order.taxAmount\`, \`order.totalAmount\` | Breakdown rows + final total. |
|
|
1597
|
+
|
|
1598
|
+
#### Rendering \`order.items[i].customizations\` by type
|
|
1599
|
+
|
|
1600
|
+
The map key is the metafield slug; the value is \`{ label, value, type }\`. Dispatch by \`type\`:
|
|
1601
|
+
|
|
1602
|
+
| Type | Value | Render |
|
|
1603
|
+
|---|---|---|
|
|
1604
|
+
| \`TEXT\`, \`TEXTAREA\`, \`URL\`, \`NUMBER\`, \`SELECT\` | string | Plain text (\`URL\` \u2192 anchor). |
|
|
1605
|
+
| \`BOOLEAN\` | \`"yes"\` / \`"no"\` | \u2713 / \u2717 |
|
|
1606
|
+
| \`MULTI_SELECT\` | \`string[]\` | Comma-separated. |
|
|
1607
|
+
| \`IMAGE\` | asset URL (string) | Thumbnail linking to full-size asset. |
|
|
1608
|
+
| \`GALLERY\` | \`string[]\` of URLs | Grid of thumbnails. |
|
|
1609
|
+
| \`COLOR\` | hex string | Swatch + hex text. |
|
|
1610
|
+
| \`DATE\` | ISO-8601 | \`toLocaleDateString()\` |
|
|
1611
|
+
| \`DATETIME\` | ISO-8601 | \`toLocaleString()\` |
|
|
1612
|
+
| Unknown | any | Plain text (defensive default). |
|
|
1613
|
+
|
|
1614
|
+
The storefront scaffolded by \`create-brainerce-store\` already implements this. See \`src/components/account/order-history.tsx\` + the sibling \`order-customizations.tsx\`, \`order-status-timeline.tsx\`, \`order-shipping-block.tsx\`, \`order-payment-block.tsx\` \u2014 mirror that structure.
|
|
1615
|
+
|
|
1616
|
+
### Do NOT render
|
|
1617
|
+
|
|
1618
|
+
These fields are NOT returned to buyers by \`/customers/me/orders\`. If you see them in older examples, ignore them \u2014 they are for merchant/admin views only.
|
|
1619
|
+
|
|
1620
|
+
- \`order.accountId\`, \`order.storeId\`, \`order.customerId\` (already known on the request)
|
|
1621
|
+
- \`order.notes\` (internal merchant notes)
|
|
1622
|
+
- \`order.customFieldValues\` (order-level checkout fields \u2014 merchant concept; buyers see \`items[i].customizations\`)
|
|
1623
|
+
- \`order.appliedSurcharges\`, \`order.surchargeAmount\`, \`order.appliedRuleIds\`, \`order.downloadMeta\`, \`order.pickupLocationData\``;
|
|
1394
1624
|
}
|
|
1395
1625
|
function getOrderConfirmationSection() {
|
|
1396
1626
|
return `## Order Confirmation Page (/order-confirmation) \u2014 REQUIRED!
|
|
@@ -1560,7 +1790,8 @@ overlay needed:
|
|
|
1560
1790
|
- \`brands[].name\`
|
|
1561
1791
|
- \`tags[].name\`
|
|
1562
1792
|
- \`variants[].name\`
|
|
1563
|
-
- \`
|
|
1793
|
+
- \`variant.attributes\` keys and values \u2014 \`getVariantOptions(variant)\` returns translated attribute names and option values automatically
|
|
1794
|
+
- \`productAttributeOptions[].attribute.name\`, \`attributeOption.name\`
|
|
1564
1795
|
- \`metafields[].value\`
|
|
1565
1796
|
|
|
1566
1797
|
**Taxonomy (\`getCategories\`, \`getBrands\`, \`getTags\`):**
|
|
@@ -1569,6 +1800,10 @@ overlay needed:
|
|
|
1569
1800
|
**Cart:**
|
|
1570
1801
|
- \`items[].product.name\`, \`items[].variant.name\`
|
|
1571
1802
|
|
|
1803
|
+
**Bundles & Order Bumps:**
|
|
1804
|
+
- \`bundleProduct.name\`, \`bundleProduct.slug\`, \`bumpProduct.name\`, \`bumpProduct.slug\`
|
|
1805
|
+
- Variant names inside bundles/bumps
|
|
1806
|
+
|
|
1572
1807
|
**Checkout:**
|
|
1573
1808
|
- Line items: \`items[].product.name\`, \`items[].variant.name\`
|
|
1574
1809
|
- Discount banners, nudges, badges
|
|
@@ -1679,6 +1914,8 @@ function getSectionByTopic(topic, connectionId, currency) {
|
|
|
1679
1914
|
return getDiscountsSection();
|
|
1680
1915
|
case "recommendations":
|
|
1681
1916
|
return getRecommendationsSection();
|
|
1917
|
+
case "product-customization-fields":
|
|
1918
|
+
return getProductCustomizationFieldsSection();
|
|
1682
1919
|
case "tax":
|
|
1683
1920
|
return getTaxDisplaySection(cur);
|
|
1684
1921
|
case "i18n":
|
|
@@ -1753,6 +1990,10 @@ function getSectionByTopic(topic, connectionId, currency) {
|
|
|
1753
1990
|
"",
|
|
1754
1991
|
"---",
|
|
1755
1992
|
"",
|
|
1993
|
+
getProductCustomizationFieldsSection(),
|
|
1994
|
+
"",
|
|
1995
|
+
"---",
|
|
1996
|
+
"",
|
|
1756
1997
|
getTaxDisplaySection(cur),
|
|
1757
1998
|
"",
|
|
1758
1999
|
"---",
|
|
@@ -1764,7 +2005,7 @@ function getSectionByTopic(topic, connectionId, currency) {
|
|
|
1764
2005
|
getAdminApiSection()
|
|
1765
2006
|
].join("\n");
|
|
1766
2007
|
default:
|
|
1767
|
-
return `Unknown topic: "${topic}". Available topics: setup, products, cart, checkout, checkout-custom-fields, payment, auth, order-confirmation, inventory, discounts, recommendations, tax, i18n, critical-rules, type-reference, admin, all`;
|
|
2008
|
+
return `Unknown topic: "${topic}". Available topics: setup, products, cart, checkout, checkout-custom-fields, payment, auth, order-confirmation, inventory, discounts, recommendations, product-customization-fields, tax, i18n, critical-rules, type-reference, admin, all`;
|
|
1768
2009
|
}
|
|
1769
2010
|
}
|
|
1770
2011
|
|
|
@@ -1784,6 +2025,7 @@ var GET_SDK_DOCS_SCHEMA = {
|
|
|
1784
2025
|
"inventory",
|
|
1785
2026
|
"discounts",
|
|
1786
2027
|
"recommendations",
|
|
2028
|
+
"product-customization-fields",
|
|
1787
2029
|
"tax",
|
|
1788
2030
|
"critical-rules",
|
|
1789
2031
|
"type-reference",
|
|
@@ -1828,6 +2070,7 @@ interface Product {
|
|
|
1828
2070
|
brands?: Array<{ id: string; name: string }>;
|
|
1829
2071
|
tags?: string[];
|
|
1830
2072
|
metafields?: ProductMetafield[];
|
|
2073
|
+
customizationFields?: ProductCustomizationField[]; // Buyer input fields \u2014 render on PDP, send values in cart metadata. Account-wide fields (MetafieldDefinition.appliesToAllProducts=true) are folded in here automatically; no client-side merging.
|
|
1831
2074
|
productAttributeOptions?: Array<{
|
|
1832
2075
|
id: string;
|
|
1833
2076
|
attributeId: string;
|
|
@@ -1886,6 +2129,32 @@ interface ProductMetafield {
|
|
|
1886
2129
|
variantId?: string | null;
|
|
1887
2130
|
}
|
|
1888
2131
|
|
|
2132
|
+
// Metafield type enum \u2014 used by ProductCustomizationField.type
|
|
2133
|
+
type MetafieldType =
|
|
2134
|
+
| 'TEXT' | 'TEXTAREA' | 'NUMBER' | 'BOOLEAN' | 'DATE' | 'DATETIME'
|
|
2135
|
+
| 'URL' | 'COLOR' | 'DIMENSION' | 'WEIGHT' | 'JSON'
|
|
2136
|
+
| 'IMAGE' | 'GALLERY'
|
|
2137
|
+
| 'SELECT' | 'MULTI_SELECT';
|
|
2138
|
+
|
|
2139
|
+
// Customer-facing input assigned per product. Render on the PDP in the order of \`position\`.
|
|
2140
|
+
// Submit values keyed by \`key\` inside AddToCartDto.metadata.
|
|
2141
|
+
// Upload files via \`uploadCustomizationFile()\` first, then place the returned \`url\` in metadata.
|
|
2142
|
+
interface ProductCustomizationField {
|
|
2143
|
+
definitionId: string;
|
|
2144
|
+
name: string; // Label to show buyers
|
|
2145
|
+
key: string; // Use as metadata key in AddToCartDto.metadata
|
|
2146
|
+
description?: string | null; // Help text
|
|
2147
|
+
type: MetafieldType;
|
|
2148
|
+
required: boolean;
|
|
2149
|
+
minLength?: number | null; // TEXT/TEXTAREA char bounds; MULTI_SELECT array bounds
|
|
2150
|
+
maxLength?: number | null;
|
|
2151
|
+
minValue?: number | null; // NUMBER bounds
|
|
2152
|
+
maxValue?: number | null;
|
|
2153
|
+
enumValues?: string[]; // REQUIRED for SELECT / MULTI_SELECT
|
|
2154
|
+
defaultValue?: string | null;
|
|
2155
|
+
position: number; // Render order
|
|
2156
|
+
}
|
|
2157
|
+
|
|
1889
2158
|
interface ProductQueryParams {
|
|
1890
2159
|
page?: number;
|
|
1891
2160
|
limit?: number;
|
|
@@ -1969,6 +2238,7 @@ interface CartItem {
|
|
|
1969
2238
|
sku?: string | null;
|
|
1970
2239
|
image?: ProductImage | string | null;
|
|
1971
2240
|
} | null;
|
|
2241
|
+
metadata?: Record<string, unknown> | null; // Buyer-submitted customization values, keyed by ProductCustomizationField.key
|
|
1972
2242
|
createdAt: string;
|
|
1973
2243
|
updatedAt: string;
|
|
1974
2244
|
}
|
|
@@ -2033,6 +2303,9 @@ interface Checkout {
|
|
|
2033
2303
|
taxAmount: string;
|
|
2034
2304
|
taxBreakdown?: TaxBreakdown | null;
|
|
2035
2305
|
total: string;
|
|
2306
|
+
surchargeAmount: string;
|
|
2307
|
+
appliedSurcharges?: Array<{ key: string; name: string; value: unknown; amount: string }> | null;
|
|
2308
|
+
customFieldValues?: Record<string, unknown> | null;
|
|
2036
2309
|
couponCode?: string | null;
|
|
2037
2310
|
lineItems: CheckoutLineItem[]; // Use THIS for order summary, NOT cart.items!
|
|
2038
2311
|
itemCount: number;
|
|
@@ -2051,6 +2324,7 @@ interface CheckoutLineItem {
|
|
|
2051
2324
|
discountAmount: string;
|
|
2052
2325
|
product: { id: string; name: string; sku: string; images?: ProductImage[]; };
|
|
2053
2326
|
variant?: { id: string; name?: string | null; sku?: string | null; image?: ProductImage | string | null; } | null;
|
|
2327
|
+
metadata?: Record<string, unknown> | null; // Copied from CartItem.metadata \u2014 buyer customization values
|
|
2054
2328
|
}
|
|
2055
2329
|
|
|
2056
2330
|
interface CheckoutAddress {
|
|
@@ -2132,6 +2406,27 @@ interface Order {
|
|
|
2132
2406
|
billingAddress?: OrderAddress;
|
|
2133
2407
|
hasDownloads?: boolean;
|
|
2134
2408
|
createdAt: string;
|
|
2409
|
+
|
|
2410
|
+
// Payment + fulfillment
|
|
2411
|
+
paymentMethod?: string | null;
|
|
2412
|
+
financialStatus?: string | null; // "pending" | "paid" | "refunded" | "partially_refunded" | "voided"
|
|
2413
|
+
fulfillmentStatus?: string | null; // "unfulfilled" | "partial" | "fulfilled"
|
|
2414
|
+
|
|
2415
|
+
// Tracking
|
|
2416
|
+
trackingNumber?: string | null;
|
|
2417
|
+
trackingUrl?: string | null;
|
|
2418
|
+
carrier?: string | null;
|
|
2419
|
+
shippedAt?: string | null; // ISO-8601
|
|
2420
|
+
deliveredAt?: string | null; // ISO-8601
|
|
2421
|
+
|
|
2422
|
+
// Timeline of status transitions, chronological
|
|
2423
|
+
statusHistory?: OrderStatusChange[] | null;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
interface OrderStatusChange {
|
|
2427
|
+
status: OrderStatus;
|
|
2428
|
+
at: string; // ISO-8601
|
|
2429
|
+
note?: string | null;
|
|
2135
2430
|
}
|
|
2136
2431
|
|
|
2137
2432
|
// \u26A0\uFE0F OrderItem is FLAT \u2014 unlike CartItem which is NESTED
|
|
@@ -2145,6 +2440,9 @@ interface OrderItem {
|
|
|
2145
2440
|
unitPrice?: string; // alias
|
|
2146
2441
|
totalPrice?: string;
|
|
2147
2442
|
image?: string; // FLAT: item.image (NOT nested)
|
|
2443
|
+
// Snapshot of buyer-submitted customization values captured at checkout.
|
|
2444
|
+
// Keyed by metafield slug. \`value\` is string[] for MULTI_SELECT / GALLERY, string otherwise.
|
|
2445
|
+
customizations?: Record<string, { label: string; value: string | string[]; type: string }>;
|
|
2148
2446
|
}
|
|
2149
2447
|
|
|
2150
2448
|
interface OrderCustomer {
|
|
@@ -2366,6 +2664,32 @@ interface ProductRecommendationsResponse {
|
|
|
2366
2664
|
}
|
|
2367
2665
|
interface CartRecommendationsResponse {
|
|
2368
2666
|
recommendations: ProductRecommendation[];
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
// Cart include types (for consolidated getCart requests)
|
|
2670
|
+
type CartIncludeOption = 'recommendations' | 'upgrades' | 'bundles';
|
|
2671
|
+
interface CartIncludeOptions {
|
|
2672
|
+
include?: CartIncludeOption[];
|
|
2673
|
+
}
|
|
2674
|
+
interface CartWithIncludes extends Cart {
|
|
2675
|
+
recommendations?: { recommendations: ProductRecommendation[] };
|
|
2676
|
+
upgrades?: { upgrades: Record<string, CartUpgradeSuggestion> };
|
|
2677
|
+
bundles?: { bundles: CartBundleOffer[] };
|
|
2678
|
+
}
|
|
2679
|
+
interface CartUpgradeSuggestion {
|
|
2680
|
+
targetProduct: ProductRecommendation;
|
|
2681
|
+
priceDelta: string;
|
|
2682
|
+
deltaPercent: number;
|
|
2683
|
+
}
|
|
2684
|
+
interface CartBundleOffer {
|
|
2685
|
+
id: string;
|
|
2686
|
+
bundleProduct: ProductRecommendation;
|
|
2687
|
+
originalPrice: string;
|
|
2688
|
+
discountedPrice: string;
|
|
2689
|
+
discountType: 'PERCENTAGE' | 'FIXED_AMOUNT';
|
|
2690
|
+
discountValue: number;
|
|
2691
|
+
requiresVariantSelection: boolean;
|
|
2692
|
+
lockedVariant?: { id: string; name?: string };
|
|
2369
2693
|
}`;
|
|
2370
2694
|
var TYPES_BY_DOMAIN = {
|
|
2371
2695
|
products: PRODUCTS_TYPES,
|
|
@@ -2429,7 +2753,8 @@ var GET_CODE_EXAMPLE_SCHEMA = {
|
|
|
2429
2753
|
"coupon-apply-and-remove",
|
|
2430
2754
|
"reservation-countdown",
|
|
2431
2755
|
"search-autocomplete-debounce",
|
|
2432
|
-
"i18n-set-locale"
|
|
2756
|
+
"i18n-set-locale",
|
|
2757
|
+
"order-history-full"
|
|
2433
2758
|
]).describe("The SDK operation to get a snippet for.")
|
|
2434
2759
|
};
|
|
2435
2760
|
var SNIPPETS = {
|
|
@@ -2524,23 +2849,51 @@ const checkout = await client.getCheckout(checkoutId);
|
|
|
2524
2849
|
"checkout-payment-providers": `// Fetch configured payment providers for this checkout.
|
|
2525
2850
|
import { client } from './brainerce';
|
|
2526
2851
|
|
|
2527
|
-
const providers = await client.getPaymentProviders(
|
|
2528
|
-
//
|
|
2529
|
-
//
|
|
2530
|
-
//
|
|
2531
|
-
//
|
|
2532
|
-
//
|
|
2533
|
-
// '
|
|
2852
|
+
const { providers, hasPayments, defaultProvider } = await client.getPaymentProviders();
|
|
2853
|
+
// each provider: { id, provider, name, publicKey, supportedMethods, testMode, isDefault, clientSdk? }
|
|
2854
|
+
//
|
|
2855
|
+
// After createPaymentIntent, the response carries a clientSdk.renderType that
|
|
2856
|
+
// tells your UI how to render the payment step. The 5 possible values:
|
|
2857
|
+
//
|
|
2858
|
+
// 'sdk-widget' \u2014 Load the provider's JS SDK (scriptUrl), then mount
|
|
2859
|
+
// into a <div id={containerId}>. Used by Stripe,
|
|
2860
|
+
// PayPal, Grow. The SDK paints its own form inside
|
|
2861
|
+
// your DOM. You control layout/placement.
|
|
2862
|
+
//
|
|
2863
|
+
// 'iframe' \u2014 Render <iframe src={clientSecret}>. Two flavors:
|
|
2864
|
+
// a) URL path contains "/embed/" \u2192 Brainerce-hosted
|
|
2865
|
+
// embed page. Render INLINE in your checkout
|
|
2866
|
+
// flow. Listen for postMessage:
|
|
2867
|
+
// - 'brainerce:resize' \u2192 update iframe height
|
|
2868
|
+
// - 'brainerce:redirect' \u2192 validated top-level
|
|
2869
|
+
// navigation (e.g. Bit express-pay)
|
|
2870
|
+
// b) Any other URL \u2192 provider-hosted page. Render
|
|
2871
|
+
// inside a modal overlay (it carries its own
|
|
2872
|
+
// branding/chrome).
|
|
2873
|
+
//
|
|
2874
|
+
// 'redirect' \u2014 Full-page navigate: window.location.href = URL.
|
|
2875
|
+
// Customer leaves your site, pays on provider page,
|
|
2876
|
+
// returns via SuccessRedirectUrl.
|
|
2877
|
+
//
|
|
2878
|
+
// 'sandbox' \u2014 Test mode. Render a "Complete Test Order" button;
|
|
2879
|
+
// call completeGuestCheckout(checkoutId). Orders are
|
|
2880
|
+
// flagged isTestOrder: true. No real charge.
|
|
2881
|
+
//
|
|
2882
|
+
// 'embedded-fields' \u2014 (Reserved \u2014 not yet shipped in any provider.) PCI
|
|
2883
|
+
// micro-iframes for sensitive fields (card number,
|
|
2884
|
+
// CVV) mounted directly into the merchant form. Full
|
|
2885
|
+
// control of surrounding layout, like Stripe
|
|
2886
|
+
// Elements. Requires the provider to ship a client
|
|
2887
|
+
// SDK that exposes mount points.
|
|
2888
|
+
//
|
|
2889
|
+
// Always branch on clientSdk.renderType \u2014 NEVER hard-code by provider name.
|
|
2890
|
+
// New providers can be added without storefront changes.
|
|
2534
2891
|
|
|
2535
|
-
// Pick the first configured provider by default (or let the user pick if
|
|
2536
|
-
// multiple are configured).
|
|
2537
|
-
const active = providers[0];
|
|
2538
|
-
|
|
2539
|
-
// If providers is empty, NO payment provider is configured and customers
|
|
2540
|
-
// cannot pay. Display a clear error \u2014 do not try to fake a checkout.
|
|
2541
2892
|
if (providers.length === 0) {
|
|
2542
2893
|
throw new Error('No payment provider configured for this store');
|
|
2543
|
-
}
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
const active = providers[0];`,
|
|
2544
2897
|
"checkout-stripe-confirm": `// Stripe confirm flow. Requires @stripe/stripe-js and @stripe/react-stripe-js
|
|
2545
2898
|
// (or the vanilla Stripe.js browser SDK).
|
|
2546
2899
|
import { loadStripe } from '@stripe/stripe-js';
|
|
@@ -2754,12 +3107,82 @@ const locale = 'he'; // e.g., from the /[locale] route segment
|
|
|
2754
3107
|
client.setLocale(locale);
|
|
2755
3108
|
|
|
2756
3109
|
// ALL content comes back translated: products, categories, brands,
|
|
2757
|
-
// tags, variants
|
|
2758
|
-
//
|
|
3110
|
+
// tags, variants (name + attributes keys/values), metafields,
|
|
3111
|
+
// cart items, checkout line items, recommendations, bundles,
|
|
3112
|
+
// order bumps, search suggestions, discount banners, nudges,
|
|
2759
3113
|
// badges, order history.
|
|
2760
3114
|
|
|
2761
3115
|
// For RTL locales (he, ar), set the document direction.
|
|
2762
|
-
// Framework-neutral: document.documentElement.setAttribute('dir', 'rtl')
|
|
3116
|
+
// Framework-neutral: document.documentElement.setAttribute('dir', 'rtl');`,
|
|
3117
|
+
"order-history-full": `// Full "My Orders" account page. Render every piece of data the server
|
|
3118
|
+
// already returns \u2014 not just order number / total / status.
|
|
3119
|
+
//
|
|
3120
|
+
// Data you get from client.getMyOrders() (already on the wire today):
|
|
3121
|
+
// - items[].customizations \u2014 buyer's custom-field entries
|
|
3122
|
+
// - statusHistory \u2014 status timeline
|
|
3123
|
+
// - shippingAddress \u2014 name + address lines + country
|
|
3124
|
+
// - trackingNumber / trackingUrl / carrier / shippedAt / deliveredAt
|
|
3125
|
+
// - paymentMethod / financialStatus / fulfillmentStatus
|
|
3126
|
+
// - appliedDiscounts \u2014 shaped buyer-facing view
|
|
3127
|
+
// - hasDownloads \u2014 call /orders/:id/downloads to fetch
|
|
3128
|
+
//
|
|
3129
|
+
// Do NOT render: accountId, storeId, customerId, notes, customFieldValues
|
|
3130
|
+
// (merchant-internal, order-level), appliedRuleIds, appliedSurcharges,
|
|
3131
|
+
// surchargeAmount, downloadMeta (raw), pickupLocationData. The backend
|
|
3132
|
+
// does not return these to buyers \u2014 if you see them in a type, they're
|
|
3133
|
+
// an oversight; still skip them.
|
|
3134
|
+
import { client } from './brainerce';
|
|
3135
|
+
import type { Order, OrderItem, OrderStatusChange } from 'brainerce';
|
|
3136
|
+
|
|
3137
|
+
const { data: orders } = await client.getMyOrders({ page: 1, limit: 10 });
|
|
3138
|
+
|
|
3139
|
+
for (const order of orders) {
|
|
3140
|
+
// Line items + per-item customizations
|
|
3141
|
+
for (const item of order.items) {
|
|
3142
|
+
// item.image, item.productName, item.quantity, item.price
|
|
3143
|
+
if (item.customizations) {
|
|
3144
|
+
for (const [fieldId, entry] of Object.entries(item.customizations)) {
|
|
3145
|
+
// entry = { label, value, type }
|
|
3146
|
+
// Type-aware render (see account-page section in get-sdk-docs):
|
|
3147
|
+
// TEXT / TEXTAREA / URL / NUMBER / SELECT \u2014 plain text
|
|
3148
|
+
// BOOLEAN \u2014 \u2713 / \u2717
|
|
3149
|
+
// MULTI_SELECT \u2014 comma-separated
|
|
3150
|
+
// IMAGE \u2014 <img src={value}>
|
|
3151
|
+
// GALLERY \u2014 wrap grid of <img>
|
|
3152
|
+
// COLOR \u2014 swatch + hex
|
|
3153
|
+
// DATE / DATETIME \u2014 localized format
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
// Status timeline \u2014 skip silently when null/empty
|
|
3159
|
+
if (order.statusHistory?.length) {
|
|
3160
|
+
for (const entry of order.statusHistory as OrderStatusChange[]) {
|
|
3161
|
+
// entry = { status, at, note? }
|
|
3162
|
+
// Render a dot + localized status label + new Date(entry.at).toLocaleString()
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
// Shipping + tracking
|
|
3167
|
+
if (order.shippingAddress) {
|
|
3168
|
+
// Render firstName lastName \xB7 line1 \xB7 line2 \xB7 city, region \xB7 postalCode \xB7 country
|
|
3169
|
+
}
|
|
3170
|
+
if (order.trackingNumber) {
|
|
3171
|
+
// Render carrier \xB7 trackingNumber, shippedAt/deliveredAt dates,
|
|
3172
|
+
// and an anchor to order.trackingUrl when present.
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
// Payment
|
|
3176
|
+
if (order.paymentMethod || order.financialStatus) {
|
|
3177
|
+
// paymentMethod: 'card' | 'paypal' | 'bank_transfer' | 'cash_on_delivery' | ...
|
|
3178
|
+
// financialStatus: 'pending' | 'paid' | 'refunded' | 'partially_refunded' | 'voided'
|
|
3179
|
+
// Map financialStatus \u2192 a colored badge.
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
// All sections above are conditional. Absent data = section not rendered \u2014
|
|
3184
|
+
// never show empty placeholders. The create-brainerce-store template's
|
|
3185
|
+
// src/components/account/order-history.tsx is the reference implementation.`
|
|
2763
3186
|
};
|
|
2764
3187
|
async function handleGetCodeExample(args) {
|
|
2765
3188
|
const snippet = SNIPPETS[args.operation];
|
|
@@ -3225,7 +3648,7 @@ var RULES = {
|
|
|
3225
3648
|
- All prices are STRINGS in the SDK. \`parseFloat\` them before math or comparisons.
|
|
3226
3649
|
- CartItem and CheckoutLineItem are NESTED (\`item.product.name\`, \`item.unitPrice\`). OrderItem is FLAT (\`item.name\`, \`item.price\`). They are not interchangeable.
|
|
3227
3650
|
- Cart has no \`.total\` field \u2014 call \`getCartTotals(cart)\` to get \`{ subtotal, tax, shipping, discount, total }\`.
|
|
3228
|
-
- \`smartGetCart()\` returns
|
|
3651
|
+
- \`smartGetCart()\` returns \`CartWithIncludes\` (extends \`Cart\`). Pass \`{ include: ['recommendations', 'upgrades', 'bundles'] }\` to fetch extras in one request.
|
|
3229
3652
|
- \`startGuestCheckout()\` is also a discriminated union. Check \`result.tracked\` before reading \`result.checkoutId\`.`
|
|
3230
3653
|
}
|
|
3231
3654
|
};
|
|
@@ -3263,6 +3686,7 @@ var GET_BUSINESS_FLOWS_SCHEMA = {
|
|
|
3263
3686
|
"order-confirmation",
|
|
3264
3687
|
"cart-persistence",
|
|
3265
3688
|
"inventory-reservation",
|
|
3689
|
+
"product-customization",
|
|
3266
3690
|
"all"
|
|
3267
3691
|
]).describe('Which flow to retrieve. Use "all" to get every flow.')
|
|
3268
3692
|
};
|
|
@@ -3382,7 +3806,7 @@ Build the OAuth button region AND the callback handler even when no providers ar
|
|
|
3382
3806
|
- **Reads:** \`client.getCart()\` returns the current cart. Call it on mount in your cart UI and on any page that shows a cart count (header).
|
|
3383
3807
|
- **Writes:** use \`client.addToCart\`, \`client.updateCartItem\`, \`client.removeCartItem\`, \`client.applyCoupon\`, \`client.removeCoupon\`. After each mutation the SDK returns the updated cart.
|
|
3384
3808
|
- **Totals:** call \`getCartTotals(cart)\` \u2014 do NOT read \`cart.total\`. The helper understands taxes, shipping, and discounts.
|
|
3385
|
-
- **\`smartGetCart()\`** returns \`
|
|
3809
|
+
- **\`smartGetCart()\`** returns \`CartWithIncludes\` (extends \`Cart\`). All carts are server-side. Pass \`{ include: ['recommendations', 'upgrades', 'bundles'] }\` to fetch extras in one request.
|
|
3386
3810
|
- **NEVER mutate cart state outside SDK helpers.** Any hand-rolled cart update risks desync with the reservation timer and the checkout flow.`
|
|
3387
3811
|
},
|
|
3388
3812
|
"inventory-reservation": {
|
|
@@ -3396,6 +3820,54 @@ Build the OAuth button region AND the callback handler even when no providers ar
|
|
|
3396
3820
|
- **On the cart page:** show per-item availability \u2014 items that can no longer be purchased (because their reservation expired or the stock dropped) should display an "out of stock" badge and the "proceed to checkout" action should be disabled until they are removed.
|
|
3397
3821
|
|
|
3398
3822
|
The reservation strategy (\`HARD\` vs \`SOFT\`) is exposed via \`get-store-capabilities\`. A HARD reservation means the stock is physically held; a SOFT reservation just tracks intent. Your UI behaves identically either way \u2014 the SDK hides the difference.`
|
|
3823
|
+
},
|
|
3824
|
+
"product-customization": {
|
|
3825
|
+
title: "Product customization (buyer input)",
|
|
3826
|
+
body: `Products can ship with \`customizationFields: ProductCustomizationField[]\` \u2014 buyer-filled inputs (engraving text, uploaded photo, pick-a-color, etc.). If you ignore them, merchants lose the data they need to fulfill the order.
|
|
3827
|
+
|
|
3828
|
+
1. **Read the fields** from the product payload. Sort by \`position\`. If the array is empty, nothing to render.
|
|
3829
|
+
2. **Render one control per field** based on \`type\`:
|
|
3830
|
+
- \`TEXT\` / \`URL\` / \`COLOR\` / \`DIMENSION\` / \`WEIGHT\` \u2192 \`<input type="text">\` (honour \`minLength\`/\`maxLength\`)
|
|
3831
|
+
- \`TEXTAREA\` \u2192 \`<textarea>\`
|
|
3832
|
+
- \`NUMBER\` \u2192 \`<input type="number">\` (honour \`minValue\`/\`maxValue\`)
|
|
3833
|
+
- \`BOOLEAN\` \u2192 checkbox (value \`true\`/\`false\`)
|
|
3834
|
+
- \`DATE\` \u2192 \`<input type="date">\` (ISO \`YYYY-MM-DD\`)
|
|
3835
|
+
- \`DATETIME\` \u2192 \`<input type="datetime-local">\` (ISO timestamp)
|
|
3836
|
+
- \`SELECT\` \u2192 \`<select>\` populated from \`enumValues\` (value = one string)
|
|
3837
|
+
- \`MULTI_SELECT\` \u2192 checkbox group from \`enumValues\` (value = \`string[]\`)
|
|
3838
|
+
- \`IMAGE\` \u2192 file input + preview (value = one URL string)
|
|
3839
|
+
- \`GALLERY\` \u2192 multi-file input (value = \`string[]\` of URLs)
|
|
3840
|
+
- \`JSON\` \u2192 advanced; render an admin-style editor or skip unless you control the data
|
|
3841
|
+
3. **For IMAGE / GALLERY types: upload FIRST, then attach the URL.** Call \`client.uploadCustomizationFile(file)\` \u2014 it returns \`{ url }\`. Put that \`url\` string into the field value. NEVER put a \`File\` object in cart metadata.
|
|
3842
|
+
4. **Enforce \`required: true\` client-side** before add-to-cart \u2014 show a red hint, block submit. The server re-validates; you want the user to fix it before the request fails.
|
|
3843
|
+
5. **Add to cart with metadata keyed by \`field.key\`:**
|
|
3844
|
+
\`\`\`ts
|
|
3845
|
+
await client.addToCart({
|
|
3846
|
+
productId,
|
|
3847
|
+
quantity: 1,
|
|
3848
|
+
metadata: {
|
|
3849
|
+
engraving_text: 'For Mom',
|
|
3850
|
+
frame_color: 'Gold',
|
|
3851
|
+
upload_photo: photoUrl, // from uploadCustomizationFile()
|
|
3852
|
+
addons: ['Gift wrap'], // MULTI_SELECT
|
|
3853
|
+
},
|
|
3854
|
+
});
|
|
3855
|
+
\`\`\`
|
|
3856
|
+
6. **On cart / checkout UIs, surface the metadata.** \`CartItem.metadata\` and \`CheckoutLineItem.metadata\` carry the values the buyer submitted. Show them in the line-item row so the buyer can verify before paying (especially uploaded image thumbnails).
|
|
3857
|
+
7. **After the order is placed, values live on \`OrderItem.customizations\`** \u2014 a \`Record<string, { label, value, type }>\` keyed by field key. Definitions may be renamed/deleted after the order; the snapshot preserves what the buyer saw at purchase time.
|
|
3858
|
+
|
|
3859
|
+
**Apply-to-all fields.** A merchant can flag a \`MetafieldDefinition\` with \`appliesToAllProducts: true\` \u2014 the backend then includes it in every product's \`customizationFields\` array automatically, including products created after the flag was set. Your client code reads \`product.customizationFields\` as-is and never merges or unions anything \u2014 just render what's there.
|
|
3860
|
+
|
|
3861
|
+
Server-side guardrails that WILL reject bad requests (so validate client-side to avoid round-trips):
|
|
3862
|
+
- Unknown keys \u2192 rejected. Only use keys that appear in \`product.customizationFields\`.
|
|
3863
|
+
- Missing \`required\` fields \u2192 rejected.
|
|
3864
|
+
- \`SELECT\` value not in \`enumValues\` \u2192 rejected.
|
|
3865
|
+
- \`MULTI_SELECT\` value not \`string[]\` OR containing values outside \`enumValues\` \u2192 rejected.
|
|
3866
|
+
- \`IMAGE\` / \`GALLERY\` values must be URLs returned from \`/customization-upload\` on this store \u2014 pasting an external URL is rejected.
|
|
3867
|
+
- Upload > 5MB or non-image MIME \u2192 rejected by upload endpoint.
|
|
3868
|
+
- More than 10 uploads per IP per minute \u2192 429.
|
|
3869
|
+
|
|
3870
|
+
Never render customization fields without also wiring the upload + metadata flow \u2014 a form that submits nothing is worse than no form at all.`
|
|
3399
3871
|
}
|
|
3400
3872
|
};
|
|
3401
3873
|
var FLOW_ORDER = [
|
|
@@ -3406,7 +3878,8 @@ var FLOW_ORDER = [
|
|
|
3406
3878
|
"oauth",
|
|
3407
3879
|
"order-confirmation",
|
|
3408
3880
|
"cart-persistence",
|
|
3409
|
-
"inventory-reservation"
|
|
3881
|
+
"inventory-reservation",
|
|
3882
|
+
"product-customization"
|
|
3410
3883
|
];
|
|
3411
3884
|
async function handleGetBusinessFlows(args) {
|
|
3412
3885
|
const header = "# Brainerce Business Flows\n\nThese sequences are framework-neutral and non-negotiable. Your framework and file layout are your choice \u2014 the order of SDK calls and the error handling is not.";
|
|
@@ -3471,6 +3944,14 @@ var FEATURES = [
|
|
|
3471
3944
|
sdk: "client.getProductBySlug(slug). Helpers: getProductPriceInfo, getVariantPrice, getStockStatus, getVariantOptions, getProductSwatches, getDescriptionContent, getProductMetafieldValue.",
|
|
3472
3945
|
mandatory: "mandatory"
|
|
3473
3946
|
},
|
|
3947
|
+
{
|
|
3948
|
+
id: "product-customization-fields",
|
|
3949
|
+
title: "Render buyer-input customization fields on the product page",
|
|
3950
|
+
description: "When a product ships with `customizationFields` (engraving text, uploaded photo, picked color, date, select / multi-select), render one control per field sorted by `position`. Enforce required + enumValues + min/max client-side. For IMAGE / GALLERY, upload via client.uploadCustomizationFile() and place the returned URL in the value. Submit all values keyed by `field.key` inside addToCart({ metadata }). Show the metadata on cart / checkout rows so the buyer can verify. Build the UI anyway \u2014 it auto-hides when a product has no customization fields. NOTE: merchants can flag a definition with `appliesToAllProducts: true`; the backend folds those into every product's `customizationFields` array automatically \u2014 the client never merges or unions anything, just reads `product.customizationFields` as-is.",
|
|
3951
|
+
sdk: "product.customizationFields, client.uploadCustomizationFile(file), client.addToCart({ productId, quantity, metadata })",
|
|
3952
|
+
flowRef: "product-customization",
|
|
3953
|
+
mandatory: "mandatory"
|
|
3954
|
+
},
|
|
3474
3955
|
{
|
|
3475
3956
|
id: "cart",
|
|
3476
3957
|
title: "Manage a cart with quantity, removal, and totals",
|