@brainerce/mcp-server 3.1.0 → 3.4.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 +910 -56
- package/dist/bin/stdio.js +910 -56
- package/dist/index.js +910 -56
- package/dist/index.mjs +910 -56
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -59,7 +59,7 @@ function getTypeQuickReference() {
|
|
|
59
59
|
| OAuth | \`provider.name\` | \`provider\` | it IS the string: \`'GOOGLE' | 'FACEBOOK' | 'GITHUB'\` |
|
|
60
60
|
| OAuth | \`oauth.url\` | \`oauth.authorizationUrl\` | |
|
|
61
61
|
| Description | \`getDescriptionContent()\` | \`{ html } | { text } | null\` | check \`'html' in content\` before use |
|
|
62
|
-
| \`smartGetCart()\` |
|
|
62
|
+
| \`smartGetCart()\` | N/A | \`cart.id\` | returns \`CartWithIncludes\` (extends \`Cart\`); pass \`{ include: [...] }\` for extras |
|
|
63
63
|
| \`startGuestCheckout()\` | always use \`.checkoutId\` | check \`result.tracked\` | discriminated union |
|
|
64
64
|
|
|
65
65
|
**Rules:**
|
|
@@ -67,8 +67,8 @@ function getTypeQuickReference() {
|
|
|
67
67
|
- CartItem/CheckoutLineItem = **NESTED** (\`item.product.name\`, \`item.unitPrice\`)
|
|
68
68
|
- OrderItem = **FLAT** (\`item.name\`, \`item.price\`, \`item.totalPrice\`, \`item.image\`)
|
|
69
69
|
- Cart has NO \`.total\` \u2014 use \`getCartTotals(cart)\`
|
|
70
|
-
- \`getCartTotals()\`
|
|
71
|
-
- \`smartGetCart()\` returns \`
|
|
70
|
+
- \`getCartTotals()\` works with server \`Cart\` (all carts are server-side now)
|
|
71
|
+
- \`smartGetCart()\` returns \`CartWithIncludes\` \u2014 always a server cart; pass \`{ include: ['recommendations', 'upgrades', 'bundles'] }\` for extras
|
|
72
72
|
- \`startGuestCheckout()\` returns discriminated union \u2014 check \`result.tracked\` before \`.checkoutId\`
|
|
73
73
|
- \`SetShippingAddressDto.email\` is **required** \u2014 always include email
|
|
74
74
|
|
|
@@ -196,7 +196,7 @@ async function startCheckout() {
|
|
|
196
196
|
6. Select shipping method \u2192 \`selectShippingMethod()\`
|
|
197
197
|
6b. (Optional) Set checkout custom fields \u2192 \`setCheckoutCustomFields()\` \u2014 surcharges auto-calculated
|
|
198
198
|
7. Create payment intent \u2192 \`createPaymentIntent()\` \u2192 returns \`{ clientSecret, provider }\`
|
|
199
|
-
8. Branch on \`provider
|
|
199
|
+
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).
|
|
200
200
|
9. **Order is created AUTOMATICALLY after payment succeeds (via webhook) \u2014 for ALL providers!**
|
|
201
201
|
|
|
202
202
|
### Checkout Page Component
|
|
@@ -299,8 +299,8 @@ function CheckoutPage() {
|
|
|
299
299
|
</div>
|
|
300
300
|
);
|
|
301
301
|
}
|
|
302
|
-
if (paymentData.
|
|
303
|
-
return <
|
|
302
|
+
if (paymentData.clientSdk?.renderType === 'iframe') {
|
|
303
|
+
return <PaymentIframe clientSecret={paymentData.clientSecret} checkoutId={paymentData.checkoutId} />;
|
|
304
304
|
}
|
|
305
305
|
if (paymentData.provider === 'paypal' && paypalClientId) {
|
|
306
306
|
return <PayPalPaymentForm clientId={paypalClientId} orderId={paymentData.clientSecret} checkoutId={paymentData.checkoutId} />;
|
|
@@ -356,26 +356,82 @@ function StripePaymentForm({ checkoutId }: { checkoutId: string }) {
|
|
|
356
356
|
}
|
|
357
357
|
\`\`\`
|
|
358
358
|
|
|
359
|
-
###
|
|
359
|
+
### Iframe-Based Providers (Cardcom, legacy Grow, etc.)
|
|
360
|
+
|
|
361
|
+
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:
|
|
362
|
+
|
|
363
|
+
**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).
|
|
364
|
+
|
|
365
|
+
**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.
|
|
366
|
+
|
|
367
|
+
Detect flavor by URL path \u2014 works across localhost/staging/prod without a domain list:
|
|
360
368
|
|
|
361
369
|
\`\`\`typescript
|
|
362
|
-
function
|
|
363
|
-
const [
|
|
370
|
+
function PaymentIframe({ clientSecret, checkoutId }: { clientSecret: string; checkoutId: string }) {
|
|
371
|
+
const [height, setHeight] = useState(540); // default before resize message arrives
|
|
372
|
+
const isBrainerceEmbed = (() => {
|
|
373
|
+
try { return new URL(clientSecret).pathname.includes('/embed/'); }
|
|
374
|
+
catch { return false; }
|
|
375
|
+
})();
|
|
376
|
+
|
|
377
|
+
useEffect(() => {
|
|
378
|
+
function handleMessage(e: MessageEvent) {
|
|
379
|
+
const data = e.data as { type?: string; height?: number; url?: string };
|
|
380
|
+
if (data?.type === 'brainerce:resize' && typeof data.height === 'number') {
|
|
381
|
+
setHeight(data.height);
|
|
382
|
+
}
|
|
383
|
+
if (data?.type === 'brainerce:redirect' && typeof data.url === 'string') {
|
|
384
|
+
// Top-level navigation (e.g. Bit). ALWAYS validate against an allowlist
|
|
385
|
+
// before navigating \u2014 never trust the URL blindly.
|
|
386
|
+
if (isTrustedPaymentUrl(data.url)) { window.top!.location.href = data.url; }
|
|
387
|
+
}
|
|
388
|
+
if (data?.type === 'brainerce:payment-complete') {
|
|
389
|
+
// Payment done \u2014 redirect to confirmation page which verifies server-side
|
|
390
|
+
window.location.href = \`/order-confirmation?checkout_id=\${checkoutId}\`;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
window.addEventListener('message', handleMessage);
|
|
394
|
+
return () => window.removeEventListener('message', handleMessage);
|
|
395
|
+
}, [checkoutId]);
|
|
396
|
+
|
|
397
|
+
if (isBrainerceEmbed) {
|
|
398
|
+
// Inline: part of the checkout flow, no overlay
|
|
399
|
+
return (
|
|
400
|
+
<div className="w-full">
|
|
401
|
+
<iframe
|
|
402
|
+
src={clientSecret}
|
|
403
|
+
style={{ width: '100%', height, border: 0, transition: 'height 0.2s ease-out' }}
|
|
404
|
+
title="Payment"
|
|
405
|
+
allow="payment"
|
|
406
|
+
/>
|
|
407
|
+
</div>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Provider-hosted page: modal overlay
|
|
364
412
|
return (
|
|
365
|
-
<div className="
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
Having trouble? <a href={paymentUrl} target="_blank" rel="noopener noreferrer" className="text-blue-600 underline">Open payment in new tab</a>
|
|
375
|
-
</p>
|
|
413
|
+
<div className="fixed inset-0 z-50 flex items-start justify-center bg-black/50 py-6 overflow-y-auto">
|
|
414
|
+
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-4xl mx-4">
|
|
415
|
+
<iframe
|
|
416
|
+
src={clientSecret}
|
|
417
|
+
style={{ width: '100%', height: '90vh', minHeight: 700, border: 0 }}
|
|
418
|
+
title="Payment"
|
|
419
|
+
allow="payment"
|
|
420
|
+
/>
|
|
421
|
+
</div>
|
|
376
422
|
</div>
|
|
377
423
|
);
|
|
378
424
|
}
|
|
425
|
+
|
|
426
|
+
// Allowlist of origins you trust for top-level payment redirects.
|
|
427
|
+
function isTrustedPaymentUrl(url: string): boolean {
|
|
428
|
+
try {
|
|
429
|
+
const u = new URL(url);
|
|
430
|
+
if (u.protocol !== 'https:') return false;
|
|
431
|
+
// Add your provider hostnames here. Example: Cardcom for Bit express-pay.
|
|
432
|
+
return u.hostname === 'cardcom.solutions' || u.hostname.endsWith('.cardcom.solutions');
|
|
433
|
+
} catch { return false; }
|
|
434
|
+
}
|
|
379
435
|
\`\`\`
|
|
380
436
|
|
|
381
437
|
### PayPal Payment Form
|
|
@@ -645,12 +701,23 @@ const growProvider = providers.find(p => p.provider === 'grow');
|
|
|
645
701
|
const paypalProvider = providers.find(p => p.provider === 'paypal');
|
|
646
702
|
\`\`\`
|
|
647
703
|
|
|
648
|
-
Each provider has: \`id\`, \`provider\` ('stripe'
|
|
704
|
+
Each provider has: \`id\`, \`provider\` (flexible string \u2014 \`'stripe'\`, \`'grow'\`, \`'paypal'\`, \`'cardcom'\`, \`'sandbox'\`, and future providers), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
|
|
705
|
+
|
|
706
|
+
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**:
|
|
649
707
|
|
|
708
|
+
- \`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.
|
|
709
|
+
- \`renderType: 'iframe'\` \u2014 render \`<iframe src={clientSecret}>\`. Two flavors detected by URL path:
|
|
710
|
+
- Path contains \`/embed/\` \u2192 Brainerce-hosted embed. Render **INLINE** (no modal). Listen for \`brainerce:resize\` / \`brainerce:redirect\` postMessages. Used by Cardcom (embedded mode).
|
|
711
|
+
- Any other URL \u2192 provider-hosted page. Render inside a **modal**. Used by Cardcom (hosted mode), legacy Grow iframe.
|
|
712
|
+
- \`renderType: 'redirect'\` \u2014 \`window.location.href = URL\`. Customer completes payment off-site and returns via SuccessRedirectUrl.
|
|
713
|
+
- \`renderType: 'sandbox'\` \u2014 show a "Complete Test Order" button; call \`completeGuestCheckout(checkoutId)\`. Orders are \`isTestOrder: true\`. Appears when \`sandboxPaymentsEnabled\` is true.
|
|
714
|
+
- \`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.
|
|
715
|
+
|
|
716
|
+
Provider-specific install notes:
|
|
650
717
|
- **Stripe:** \`npm install @stripe/stripe-js @stripe/react-stripe-js\` \u2014 \`loadStripe(publicKey, { stripeAccount })\`
|
|
651
|
-
- **Grow:** No SDK \u2014 uses iframe with payment URL. Supports credit cards, Bit, Apple Pay, Google Pay.
|
|
652
718
|
- **PayPal:** \`npm install @paypal/react-paypal-js\` \u2014 \`PayPalScriptProvider\` + \`PayPalButtons\`
|
|
653
|
-
- **
|
|
719
|
+
- **Grow:** No SDK needed \u2014 JS SDK loaded via \`clientSdk.scriptUrl\`. Supports credit cards, Bit, Apple Pay, Google Pay.
|
|
720
|
+
- **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.`;
|
|
654
721
|
}
|
|
655
722
|
function getProductsSection(_currency) {
|
|
656
723
|
return `## Products & Variants
|
|
@@ -681,10 +748,12 @@ When \`storeInfo.i18n.enabled\` is true, call \`client.setLocale(locale)\` once
|
|
|
681
748
|
\`{ locale }\` params needed. Translated fields include: \`name\`,
|
|
682
749
|
\`description\`, \`slug\`, \`seoTitle\`, \`seoDescription\`, plus
|
|
683
750
|
\`categories[].name\`, \`brands[].name\`, \`tags[].name\`, \`variants[].name\`,
|
|
684
|
-
\`
|
|
685
|
-
\`
|
|
686
|
-
|
|
687
|
-
and
|
|
751
|
+
\`variant.attributes\` keys and values (so \`getVariantOptions()\` returns
|
|
752
|
+
translated attribute/option names), \`productAttributeOptions[].attribute.name\`/
|
|
753
|
+
\`attributeOption.name\`, \`metafields[].value\`, cart item product/variant names,
|
|
754
|
+
and recommendation/bundle/bump product names. No client-side overlay. See the
|
|
755
|
+
full \`i18n\` section (\`get-sdk-docs\` topic \`i18n\`) for the \`[locale]\` route
|
|
756
|
+
pattern, SDK setup, and the brand/tag badge examples.
|
|
688
757
|
|
|
689
758
|
### Price Display (Use SDK Helper!)
|
|
690
759
|
|
|
@@ -821,6 +890,8 @@ const material = getProductMetafieldValue(product, 'material');
|
|
|
821
890
|
|
|
822
891
|
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.
|
|
823
892
|
|
|
893
|
+
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\`.
|
|
894
|
+
|
|
824
895
|
\`\`\`typescript
|
|
825
896
|
import { getProductCustomizationFields } from 'brainerce';
|
|
826
897
|
import type { ProductCustomizationField } from 'brainerce';
|
|
@@ -879,7 +950,7 @@ function getCartSection(_currency) {
|
|
|
879
950
|
### Smart Cart Methods (RECOMMENDED)
|
|
880
951
|
|
|
881
952
|
\`\`\`typescript
|
|
882
|
-
const cart = await client.smartGetCart(); // Returns
|
|
953
|
+
const cart = await client.smartGetCart(); // Returns CartWithIncludes (extends Cart)
|
|
883
954
|
await client.smartAddToCart({
|
|
884
955
|
productId: product.id,
|
|
885
956
|
variantId: selectedVariant?.id,
|
|
@@ -1010,7 +1081,19 @@ const recs = (product as any).recommendations as ProductRecommendationsResponse
|
|
|
1010
1081
|
|
|
1011
1082
|
### Cart Page Features
|
|
1012
1083
|
|
|
1013
|
-
**
|
|
1084
|
+
**Consolidated include** (fetch recommendations, upgrades, and bundles in one request):
|
|
1085
|
+
\`\`\`typescript
|
|
1086
|
+
import type { CartWithIncludes } from 'brainerce';
|
|
1087
|
+
const cart = await client.getCart(cartId, {
|
|
1088
|
+
include: ['recommendations', 'upgrades', 'bundles'],
|
|
1089
|
+
});
|
|
1090
|
+
// cart.recommendations \u2014 cross-sell recommendations
|
|
1091
|
+
// cart.upgrades \u2014 upgrade suggestions keyed by source product ID
|
|
1092
|
+
// cart.bundles \u2014 bundle offers
|
|
1093
|
+
// Also works with smartGetCart: client.smartGetCart({ include: ['recommendations', 'upgrades', 'bundles'] })
|
|
1094
|
+
\`\`\`
|
|
1095
|
+
|
|
1096
|
+
**Cross-sell recommendations** (or fetch individually for targeted refresh):
|
|
1014
1097
|
\`\`\`typescript
|
|
1015
1098
|
import type { CartRecommendationsResponse } from 'brainerce';
|
|
1016
1099
|
const cartRecs = await client.getCartRecommendations(cartId, 4);
|
|
@@ -1102,6 +1185,110 @@ Always check the flag before rendering: \`if (storeInfo?.upsell?.featureName !==
|
|
|
1102
1185
|
ProductRecommendation: \`id\`, \`name\`, \`slug\`, \`basePrice\`, \`salePrice\`, \`images\`, \`type\`, \`inventory\`, \`relationType\`, \`variants?\` (when variant selection needed).
|
|
1103
1186
|
OrderBump/CartBundleOffer: includes \`requiresVariantSelection\` (boolean) and \`lockedVariant?\` (\`{ id, name, attributes }\`).`;
|
|
1104
1187
|
}
|
|
1188
|
+
function getProductCustomizationFieldsSection() {
|
|
1189
|
+
return `## Product Customization Fields (buyer input on product page)
|
|
1190
|
+
|
|
1191
|
+
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.
|
|
1192
|
+
|
|
1193
|
+
### Where the fields come from
|
|
1194
|
+
|
|
1195
|
+
They arrive **embedded on the product response** \u2014 no extra API call:
|
|
1196
|
+
|
|
1197
|
+
\`\`\`typescript
|
|
1198
|
+
import type { Product, ProductCustomizationField, MetafieldType } from 'brainerce';
|
|
1199
|
+
|
|
1200
|
+
const product = await client.getProductBySlug(slug);
|
|
1201
|
+
const fields: ProductCustomizationField[] = product.customizationFields ?? [];
|
|
1202
|
+
// fields is sorted by position. If empty, render the product page normally.
|
|
1203
|
+
\`\`\`
|
|
1204
|
+
|
|
1205
|
+
Each field has:
|
|
1206
|
+
\`\`\`typescript
|
|
1207
|
+
{
|
|
1208
|
+
definitionId: string;
|
|
1209
|
+
key: string; // stable identifier \u2014 use this as the metadata key
|
|
1210
|
+
name: string; // display label (may be localized)
|
|
1211
|
+
description?: string | null;
|
|
1212
|
+
type: MetafieldType; // 'TEXT' | 'TEXTAREA' | 'NUMBER' | 'BOOLEAN' | 'DATE' | 'DATETIME' |
|
|
1213
|
+
// 'JSON' | 'URL' | 'COLOR' | 'DIMENSION' | 'WEIGHT' |
|
|
1214
|
+
// 'IMAGE' | 'GALLERY' | 'SELECT' | 'MULTI_SELECT'
|
|
1215
|
+
required: boolean;
|
|
1216
|
+
minLength?: number | null; // chars for TEXT/TEXTAREA; array size for MULTI_SELECT
|
|
1217
|
+
maxLength?: number | null;
|
|
1218
|
+
minValue?: number | null; // NUMBER / DIMENSION / WEIGHT
|
|
1219
|
+
maxValue?: number | null;
|
|
1220
|
+
enumValues?: string[]; // required for SELECT / MULTI_SELECT
|
|
1221
|
+
defaultValue?: string | null;
|
|
1222
|
+
position: number;
|
|
1223
|
+
}
|
|
1224
|
+
\`\`\`
|
|
1225
|
+
|
|
1226
|
+
### Render one control per type
|
|
1227
|
+
|
|
1228
|
+
| type | Render as | Collected value |
|
|
1229
|
+
| --------------- | ---------------------------------------------------------- | ---------------------------- |
|
|
1230
|
+
| \`TEXT\` | \`<input type="text">\` | \`string\` |
|
|
1231
|
+
| \`TEXTAREA\` | \`<textarea>\` | \`string\` |
|
|
1232
|
+
| \`NUMBER\` | \`<input type="number">\` | \`number\` |
|
|
1233
|
+
| \`BOOLEAN\` | checkbox / switch | \`boolean\` |
|
|
1234
|
+
| \`DATE\` | \`<input type="date">\` | \`string\` (YYYY-MM-DD) |
|
|
1235
|
+
| \`DATETIME\` | \`<input type="datetime-local">\` | \`string\` (ISO 8601) |
|
|
1236
|
+
| \`URL\` | \`<input type="url">\` | \`string\` |
|
|
1237
|
+
| \`COLOR\` | \`<input type="color">\` | \`string\` (#RRGGBB) |
|
|
1238
|
+
| \`SELECT\` | \`<select>\` or radio group (use \`enumValues\`) | \`string\` (in enumValues) |
|
|
1239
|
+
| \`MULTI_SELECT\`| checkbox group (use \`enumValues\`) | \`string[]\` (every in enum) |
|
|
1240
|
+
| \`IMAGE\` | file input + upload via \`uploadCustomizationFile\` | \`string\` (URL) |
|
|
1241
|
+
| \`GALLERY\` | multi-file input + one upload per file | \`string[]\` (URLs) |
|
|
1242
|
+
| \`JSON\` | textarea; validate is parseable | \`string\` (JSON) |
|
|
1243
|
+
|
|
1244
|
+
### Upload buyer-submitted images
|
|
1245
|
+
|
|
1246
|
+
\`IMAGE\` and \`GALLERY\` require uploading before add-to-cart:
|
|
1247
|
+
|
|
1248
|
+
\`\`\`typescript
|
|
1249
|
+
const { url } = await client.uploadCustomizationFile(file);
|
|
1250
|
+
// Server rules: image/* only, max 5 MB, 10 uploads/min per IP.
|
|
1251
|
+
// Files are retained for at least 7 days; after that, if the cart never
|
|
1252
|
+
// became an order, they are automatically deleted.
|
|
1253
|
+
\`\`\`
|
|
1254
|
+
|
|
1255
|
+
### Add to cart with customization metadata
|
|
1256
|
+
|
|
1257
|
+
\`\`\`typescript
|
|
1258
|
+
// Collect values keyed by field.key \u2014 NOT by field.name or field.definitionId.
|
|
1259
|
+
const metadata: Record<string, unknown> = {
|
|
1260
|
+
engraving_text: 'Happy Birthday!', // TEXT
|
|
1261
|
+
frame_color: 'Gold', // SELECT (must be in enumValues)
|
|
1262
|
+
upload_photo: url, // IMAGE (URL from uploadCustomizationFile)
|
|
1263
|
+
addons: ['Gift wrap'], // MULTI_SELECT (always an array)
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
await client.addToCart(cart.id, {
|
|
1267
|
+
productId: product.id,
|
|
1268
|
+
quantity: 1,
|
|
1269
|
+
metadata,
|
|
1270
|
+
});
|
|
1271
|
+
\`\`\`
|
|
1272
|
+
|
|
1273
|
+
Server validation (rejected with HTTP 400 on failure):
|
|
1274
|
+
- \`required: true\` \u2192 must be present and non-empty
|
|
1275
|
+
- \`TEXT\` / \`TEXTAREA\` \u2192 string; \`minLength\` / \`maxLength\` enforced as character count
|
|
1276
|
+
- \`NUMBER\` \u2192 \`minValue\` / \`maxValue\` enforced
|
|
1277
|
+
- \`SELECT\` \u2192 value must be one of \`enumValues\`
|
|
1278
|
+
- \`MULTI_SELECT\` \u2192 array; each element in \`enumValues\`; duplicates removed; \`minLength\` / \`maxLength\` enforced as array size
|
|
1279
|
+
- \`IMAGE\` / \`GALLERY\` \u2192 URL(s) must be from \`/customization-upload\` on this store
|
|
1280
|
+
|
|
1281
|
+
### Order snapshot
|
|
1282
|
+
|
|
1283
|
+
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.
|
|
1284
|
+
|
|
1285
|
+
### Common mistakes
|
|
1286
|
+
|
|
1287
|
+
- Passing values keyed by \`field.name\` instead of \`field.key\` \u2192 silently ignored (unknown key)
|
|
1288
|
+
- Sending a \`MULTI_SELECT\` as a string instead of \`string[]\` \u2192 HTTP 400
|
|
1289
|
+
- Using a raw external URL (not from \`/customization-upload\`) for \`IMAGE\` / \`GALLERY\` \u2192 HTTP 400
|
|
1290
|
+
- Assuming \`enumValues\` is present for all types \u2014 only \`SELECT\` / \`MULTI_SELECT\` require it`;
|
|
1291
|
+
}
|
|
1105
1292
|
function getInventorySection() {
|
|
1106
1293
|
return `## Inventory, Stock Display & Reservation Countdown
|
|
1107
1294
|
|
|
@@ -1358,7 +1545,50 @@ const { data: orders } = await client.getMyOrders({ page: 1, limit: 10 }); // Re
|
|
|
1358
1545
|
// \u274C WRONG \u2014 these methods don't exist!
|
|
1359
1546
|
client.getCustomerProfile();
|
|
1360
1547
|
client.getCustomerOrders();
|
|
1361
|
-
|
|
1548
|
+
\`\`\`
|
|
1549
|
+
|
|
1550
|
+
### Order history should show more than just totals
|
|
1551
|
+
|
|
1552
|
+
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.
|
|
1553
|
+
|
|
1554
|
+
| Section | Source field | Notes |
|
|
1555
|
+
|---|---|---|
|
|
1556
|
+
| Header (number, status badge, date, total) | \`order.orderNumber\`, \`order.status\`, \`order.createdAt\`, \`order.totalAmount\` | Always present. |
|
|
1557
|
+
| Line items | \`order.items[]\` | Render image, name, qty, price. |
|
|
1558
|
+
| **Per-item customizations** | \`order.items[i].customizations\` | Map of { label, value, type }. Render by \`type\` \u2014 see table below. |
|
|
1559
|
+
| **Status timeline** | \`order.statusHistory\` | \`OrderStatusChange[]\`: \`{ status, at, note? }\`. Render as a vertical list. |
|
|
1560
|
+
| **Shipping address** | \`order.shippingAddress\` | Standard \`OrderAddress\` shape. |
|
|
1561
|
+
| **Tracking** | \`order.trackingNumber\`, \`order.trackingUrl\`, \`order.carrier\`, \`order.shippedAt\`, \`order.deliveredAt\` | Link out to \`trackingUrl\` when set. |
|
|
1562
|
+
| **Payment** | \`order.paymentMethod\`, \`order.financialStatus\` | Badge \`financialStatus\` (paid / pending / refunded / partially_refunded). |
|
|
1563
|
+
| Downloads | \`order.hasDownloads\` \u2192 \`client.getOrderDownloads(id)\` | Separate call; returns \`OrderDownloadLink[]\`. |
|
|
1564
|
+
| Financial summary | \`order.subtotal\`, \`order.appliedDiscounts\`, \`order.couponCode\` + \`couponDiscount\`, \`order.shippingAmount\`, \`order.taxAmount\`, \`order.totalAmount\` | Breakdown rows + final total. |
|
|
1565
|
+
|
|
1566
|
+
#### Rendering \`order.items[i].customizations\` by type
|
|
1567
|
+
|
|
1568
|
+
The map key is the metafield slug; the value is \`{ label, value, type }\`. Dispatch by \`type\`:
|
|
1569
|
+
|
|
1570
|
+
| Type | Value | Render |
|
|
1571
|
+
|---|---|---|
|
|
1572
|
+
| \`TEXT\`, \`TEXTAREA\`, \`URL\`, \`NUMBER\`, \`SELECT\` | string | Plain text (\`URL\` \u2192 anchor). |
|
|
1573
|
+
| \`BOOLEAN\` | \`"yes"\` / \`"no"\` | \u2713 / \u2717 |
|
|
1574
|
+
| \`MULTI_SELECT\` | \`string[]\` | Comma-separated. |
|
|
1575
|
+
| \`IMAGE\` | asset URL (string) | Thumbnail linking to full-size asset. |
|
|
1576
|
+
| \`GALLERY\` | \`string[]\` of URLs | Grid of thumbnails. |
|
|
1577
|
+
| \`COLOR\` | hex string | Swatch + hex text. |
|
|
1578
|
+
| \`DATE\` | ISO-8601 | \`toLocaleDateString()\` |
|
|
1579
|
+
| \`DATETIME\` | ISO-8601 | \`toLocaleString()\` |
|
|
1580
|
+
| Unknown | any | Plain text (defensive default). |
|
|
1581
|
+
|
|
1582
|
+
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.
|
|
1583
|
+
|
|
1584
|
+
### Do NOT render
|
|
1585
|
+
|
|
1586
|
+
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.
|
|
1587
|
+
|
|
1588
|
+
- \`order.accountId\`, \`order.storeId\`, \`order.customerId\` (already known on the request)
|
|
1589
|
+
- \`order.notes\` (internal merchant notes)
|
|
1590
|
+
- \`order.customFieldValues\` (order-level checkout fields \u2014 merchant concept; buyers see \`items[i].customizations\`)
|
|
1591
|
+
- \`order.appliedSurcharges\`, \`order.surchargeAmount\`, \`order.appliedRuleIds\`, \`order.downloadMeta\`, \`order.pickupLocationData\``;
|
|
1362
1592
|
}
|
|
1363
1593
|
function getOrderConfirmationSection() {
|
|
1364
1594
|
return `## Order Confirmation Page (/order-confirmation) \u2014 REQUIRED!
|
|
@@ -1528,7 +1758,8 @@ overlay needed:
|
|
|
1528
1758
|
- \`brands[].name\`
|
|
1529
1759
|
- \`tags[].name\`
|
|
1530
1760
|
- \`variants[].name\`
|
|
1531
|
-
- \`
|
|
1761
|
+
- \`variant.attributes\` keys and values \u2014 \`getVariantOptions(variant)\` returns translated attribute names and option values automatically
|
|
1762
|
+
- \`productAttributeOptions[].attribute.name\`, \`attributeOption.name\`
|
|
1532
1763
|
- \`metafields[].value\`
|
|
1533
1764
|
|
|
1534
1765
|
**Taxonomy (\`getCategories\`, \`getBrands\`, \`getTags\`):**
|
|
@@ -1537,6 +1768,10 @@ overlay needed:
|
|
|
1537
1768
|
**Cart:**
|
|
1538
1769
|
- \`items[].product.name\`, \`items[].variant.name\`
|
|
1539
1770
|
|
|
1771
|
+
**Bundles & Order Bumps:**
|
|
1772
|
+
- \`bundleProduct.name\`, \`bundleProduct.slug\`, \`bumpProduct.name\`, \`bumpProduct.slug\`
|
|
1773
|
+
- Variant names inside bundles/bumps
|
|
1774
|
+
|
|
1540
1775
|
**Checkout:**
|
|
1541
1776
|
- Line items: \`items[].product.name\`, \`items[].variant.name\`
|
|
1542
1777
|
- Discount banners, nudges, badges
|
|
@@ -1621,6 +1856,265 @@ const admin = new BrainerceClient({ apiKey: 'brainerce_...' });
|
|
|
1621
1856
|
// OAuth: getOAuthProviders(), configureOAuthProvider()
|
|
1622
1857
|
\`\`\``;
|
|
1623
1858
|
}
|
|
1859
|
+
function getContactInquiriesSection() {
|
|
1860
|
+
return `## Contact Inquiries & Forms (optional)
|
|
1861
|
+
|
|
1862
|
+
Accept messages from one or more contact forms on your storefront. The merchant
|
|
1863
|
+
configures everything in the Brainerce dashboard under **Customers \u2192 Contact
|
|
1864
|
+
Forms**; submissions land in **Customers \u2192 Inquiries** and both sides receive
|
|
1865
|
+
email notifications.
|
|
1866
|
+
|
|
1867
|
+
**IMPORTANT for AI code generators:** the merchant OWNS the form shape. Do not
|
|
1868
|
+
hardcode labels, placeholders, help text, the submit button, or the success
|
|
1869
|
+
message. Render from the schema returned by \`contactForms.get()\` so the
|
|
1870
|
+
storefront reflects whatever the merchant configures \u2014 including new fields
|
|
1871
|
+
they add later, translations they add later, and the success message they
|
|
1872
|
+
write. This is the difference between "a contact form" and "THE merchant's
|
|
1873
|
+
contact form."
|
|
1874
|
+
|
|
1875
|
+
### Two integration paths
|
|
1876
|
+
|
|
1877
|
+
1. **Simple (legacy)** \u2014 pass \`{ name, email, subject, message }\` and submit.
|
|
1878
|
+
Works out of the box, no dashboard setup needed. Backed by a lazily-seeded
|
|
1879
|
+
default form keyed \`"main"\` with five built-in fields (\`name\`, \`email\`,
|
|
1880
|
+
\`phone\`, \`subject\`, \`message\`). Choose this only for throwaway demos.
|
|
1881
|
+
2. **Flexible (recommended, default in \`npx create-brainerce-store\`)** \u2014
|
|
1882
|
+
merchants can configure multiple forms per store (e.g. \`main\`,
|
|
1883
|
+
\`newsletter\`, \`whatsapp_prechat\`), add custom fields (TEXT, TEXTAREA,
|
|
1884
|
+
EMAIL, PHONE, NUMBER, SELECT, MULTI_SELECT, CHECKBOX, URL, DATE), translate
|
|
1885
|
+
everything per locale, hide any non-essential built-in. Fetch the schema
|
|
1886
|
+
with \`contactForms.get(formKey, locale)\` and render dynamically.
|
|
1887
|
+
|
|
1888
|
+
### Simple path (legacy shape)
|
|
1889
|
+
|
|
1890
|
+
\`\`\`typescript
|
|
1891
|
+
import type { CreateInquiryInput, CreateInquiryResponse } from 'brainerce';
|
|
1892
|
+
|
|
1893
|
+
const result = await brainerce.createInquiry({
|
|
1894
|
+
name: 'Jane Doe',
|
|
1895
|
+
email: 'jane@example.com',
|
|
1896
|
+
subject: 'Question about shipping',
|
|
1897
|
+
message: 'Hi, do you ship internationally?',
|
|
1898
|
+
phone: '+1-555-0100', // optional
|
|
1899
|
+
customerId: customer?.id, // optional \u2014 link to logged-in customer
|
|
1900
|
+
metadata: { page: '/contact' }, // optional
|
|
1901
|
+
});
|
|
1902
|
+
// \u2192 { id, status: 'NEW', createdAt }
|
|
1903
|
+
\`\`\`
|
|
1904
|
+
|
|
1905
|
+
### Flexible path \u2014 end-to-end contract
|
|
1906
|
+
|
|
1907
|
+
**Endpoints:**
|
|
1908
|
+
|
|
1909
|
+
| Method | Path | Returns |
|
|
1910
|
+
| ------ | ----------------------------------------------------------------- | ------------------------------ |
|
|
1911
|
+
| GET | \`/stores/:storeId/contact-forms\` | \`ContactFormSummary[]\` |
|
|
1912
|
+
| GET | \`/stores/:storeId/contact-forms/:formKey?locale=xx\` | \`ContactFormPublic\` |
|
|
1913
|
+
| POST | \`/stores/:storeId/inquiries\` | \`CreateInquiryResponse\` |
|
|
1914
|
+
|
|
1915
|
+
All three are public (no API key). The SDK wraps them so you never call them
|
|
1916
|
+
directly \u2014 use \`brainerce.contactForms.list()\`, \`brainerce.contactForms.get()\`,
|
|
1917
|
+
and \`brainerce.createInquiry()\`.
|
|
1918
|
+
|
|
1919
|
+
\`\`\`typescript
|
|
1920
|
+
import type { ContactFormPublic } from 'brainerce';
|
|
1921
|
+
|
|
1922
|
+
// List active forms (optional \u2014 if you want a form picker or route-per-form setup)
|
|
1923
|
+
const forms = await brainerce.contactForms.list();
|
|
1924
|
+
// \u2192 [{ key: 'main', name: 'Contact us', isDefault: true }, ...]
|
|
1925
|
+
|
|
1926
|
+
// Fetch one form's schema \u2014 server pre-resolves translations for the locale
|
|
1927
|
+
// and strips every field the merchant marked isVisible=false.
|
|
1928
|
+
const form = await brainerce.contactForms.get('main', 'en');
|
|
1929
|
+
// \u2192 {
|
|
1930
|
+
// id, key, name, description?, submitButton, successMessage,
|
|
1931
|
+
// fields: [{ key, type, label, placeholder?, helpText?, isRequired,
|
|
1932
|
+
// enumValues?, validation?, defaultValue? }, ...]
|
|
1933
|
+
// }
|
|
1934
|
+
|
|
1935
|
+
// Submit a keyed payload. Unknown keys are stripped, required fields are
|
|
1936
|
+
// enforced, and every value is validated against \`validation\` server-side.
|
|
1937
|
+
await brainerce.createInquiry({
|
|
1938
|
+
formKey: 'main',
|
|
1939
|
+
fields: {
|
|
1940
|
+
email: 'jane@example.com', // built-in keys
|
|
1941
|
+
message: 'Hi...',
|
|
1942
|
+
// ...any custom keys the merchant added via the dashboard
|
|
1943
|
+
},
|
|
1944
|
+
locale: 'en', // stored on the inquiry \u2014 lets staff filter by language
|
|
1945
|
+
sourceMetadata: { page: '/contact' }, // arbitrary provenance (UTM, referrer, etc.)
|
|
1946
|
+
});
|
|
1947
|
+
\`\`\`
|
|
1948
|
+
|
|
1949
|
+
Both shapes go to the same POST endpoint and may be mixed; \`fields\` wins when
|
|
1950
|
+
both provide the same key. Unknown keys (not defined on the form schema) are
|
|
1951
|
+
stripped server-side.
|
|
1952
|
+
|
|
1953
|
+
### Dynamic rendering \u2014 reference implementation
|
|
1954
|
+
|
|
1955
|
+
Render every field type the merchant can pick. Keep this as a \`<DynamicField>\`
|
|
1956
|
+
component so the form is fully driven by \`schema.fields\`.
|
|
1957
|
+
|
|
1958
|
+
\`\`\`tsx
|
|
1959
|
+
import type { ContactFormPublic, ContactFormPublicField } from 'brainerce';
|
|
1960
|
+
|
|
1961
|
+
type FieldValue = string | string[] | boolean;
|
|
1962
|
+
|
|
1963
|
+
function defaultValueFor(f: ContactFormPublicField): FieldValue {
|
|
1964
|
+
if (f.type === 'CHECKBOX') return false;
|
|
1965
|
+
if (f.type === 'MULTI_SELECT') return [];
|
|
1966
|
+
return f.defaultValue ?? '';
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
function isEmpty(v: FieldValue): boolean {
|
|
1970
|
+
if (typeof v === 'string') return v.trim().length === 0;
|
|
1971
|
+
if (Array.isArray(v)) return v.length === 0;
|
|
1972
|
+
return v === false;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
function DynamicField({
|
|
1976
|
+
field,
|
|
1977
|
+
value,
|
|
1978
|
+
onChange,
|
|
1979
|
+
}: {
|
|
1980
|
+
field: ContactFormPublicField;
|
|
1981
|
+
value: FieldValue;
|
|
1982
|
+
onChange: (v: FieldValue) => void;
|
|
1983
|
+
}) {
|
|
1984
|
+
const id = \`contact-\${field.key}\`;
|
|
1985
|
+
const { minLength, maxLength, min, max, pattern } = field.validation ?? {};
|
|
1986
|
+
const strVal = typeof value === 'string' ? value : '';
|
|
1987
|
+
|
|
1988
|
+
// Label \u2014 USE \`field.label\`, NEVER hardcode. Falls back to the key.
|
|
1989
|
+
const label = (
|
|
1990
|
+
<label htmlFor={id} className="mb-1.5 block text-sm font-medium">
|
|
1991
|
+
{field.label}
|
|
1992
|
+
{field.isRequired && <span aria-hidden className="text-red-500"> *</span>}
|
|
1993
|
+
</label>
|
|
1994
|
+
);
|
|
1995
|
+
const help = field.helpText ? <p className="mt-1 text-xs opacity-70">{field.helpText}</p> : null;
|
|
1996
|
+
|
|
1997
|
+
switch (field.type) {
|
|
1998
|
+
case 'TEXTAREA':
|
|
1999
|
+
return (<div>{label}<textarea id={id} required={field.isRequired} maxLength={maxLength} minLength={minLength} rows={6} placeholder={field.placeholder} value={strVal} onChange={(e) => onChange(e.target.value)} />{help}</div>);
|
|
2000
|
+
case 'EMAIL':
|
|
2001
|
+
return (<div>{label}<input id={id} type="email" required={field.isRequired} autoComplete="email" placeholder={field.placeholder} value={strVal} onChange={(e) => onChange(e.target.value)} />{help}</div>);
|
|
2002
|
+
case 'PHONE':
|
|
2003
|
+
return (<div>{label}<input id={id} type="tel" required={field.isRequired} autoComplete="tel" placeholder={field.placeholder} value={strVal} onChange={(e) => onChange(e.target.value)} />{help}</div>);
|
|
2004
|
+
case 'URL':
|
|
2005
|
+
return (<div>{label}<input id={id} type="url" required={field.isRequired} placeholder={field.placeholder} value={strVal} onChange={(e) => onChange(e.target.value)} />{help}</div>);
|
|
2006
|
+
case 'NUMBER':
|
|
2007
|
+
return (<div>{label}<input id={id} type="number" required={field.isRequired} min={min} max={max} placeholder={field.placeholder} value={strVal} onChange={(e) => onChange(e.target.value)} />{help}</div>);
|
|
2008
|
+
case 'DATE':
|
|
2009
|
+
return (<div>{label}<input id={id} type="date" required={field.isRequired} value={strVal} onChange={(e) => onChange(e.target.value)} />{help}</div>);
|
|
2010
|
+
case 'SELECT':
|
|
2011
|
+
return (<div>{label}<select id={id} required={field.isRequired} value={strVal} onChange={(e) => onChange(e.target.value)}><option value="">\u2014</option>{field.enumValues?.map((o) => (<option key={o.value} value={o.value}>{o.label}</option>))}</select>{help}</div>);
|
|
2012
|
+
case 'MULTI_SELECT': {
|
|
2013
|
+
const arr = Array.isArray(value) ? value : [];
|
|
2014
|
+
return (<div>{label}<div>{field.enumValues?.map((o) => { const checked = arr.includes(o.value); return (<label key={o.value}><input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked ? [...arr, o.value] : arr.filter((v) => v !== o.value))} /><span>{o.label}</span></label>); })}</div>{help}</div>);
|
|
2015
|
+
}
|
|
2016
|
+
case 'CHECKBOX':
|
|
2017
|
+
return (<div><label htmlFor={id}><input id={id} type="checkbox" required={field.isRequired} checked={value === true} onChange={(e) => onChange(e.target.checked)} /><span>{field.label}</span></label>{help}</div>);
|
|
2018
|
+
case 'TEXT':
|
|
2019
|
+
default:
|
|
2020
|
+
return (<div>{label}<input id={id} type="text" required={field.isRequired} maxLength={maxLength} minLength={minLength} pattern={pattern} placeholder={field.placeholder} value={strVal} onChange={(e) => onChange(e.target.value)} />{help}</div>);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
export function ContactPage() {
|
|
2025
|
+
const [schema, setSchema] = useState<ContactFormPublic | null>(null);
|
|
2026
|
+
const [values, setValues] = useState<Record<string, FieldValue>>({});
|
|
2027
|
+
const [honeypot, setHoneypot] = useState('');
|
|
2028
|
+
const [sent, setSent] = useState(false);
|
|
2029
|
+
const [loading, setLoading] = useState(false);
|
|
2030
|
+
|
|
2031
|
+
// Read the current locale from the <html lang> attribute (or your own i18n context).
|
|
2032
|
+
const locale = typeof document !== 'undefined' ? document.documentElement.lang || undefined : undefined;
|
|
2033
|
+
|
|
2034
|
+
useEffect(() => {
|
|
2035
|
+
brainerce.contactForms.get('main', locale).then((form) => {
|
|
2036
|
+
setSchema(form);
|
|
2037
|
+
const initial: Record<string, FieldValue> = {};
|
|
2038
|
+
for (const f of form.fields) initial[f.key] = defaultValueFor(f);
|
|
2039
|
+
setValues(initial);
|
|
2040
|
+
});
|
|
2041
|
+
}, [locale]);
|
|
2042
|
+
|
|
2043
|
+
if (!schema) return null;
|
|
2044
|
+
|
|
2045
|
+
const onSubmit = async (e: React.FormEvent) => {
|
|
2046
|
+
e.preventDefault();
|
|
2047
|
+
if (loading) return;
|
|
2048
|
+
if (honeypot.trim().length > 0) { setSent(true); return; } // bot \u2192 silent success
|
|
2049
|
+
|
|
2050
|
+
setLoading(true);
|
|
2051
|
+
try {
|
|
2052
|
+
const payload: Record<string, unknown> = {};
|
|
2053
|
+
for (const f of schema.fields) {
|
|
2054
|
+
const raw = values[f.key];
|
|
2055
|
+
if (isEmpty(raw)) continue;
|
|
2056
|
+
payload[f.key] = typeof raw === 'string' ? raw.trim() : raw;
|
|
2057
|
+
}
|
|
2058
|
+
await brainerce.createInquiry({ formKey: schema.key, fields: payload, locale });
|
|
2059
|
+
setSent(true);
|
|
2060
|
+
} finally {
|
|
2061
|
+
setLoading(false);
|
|
2062
|
+
}
|
|
2063
|
+
};
|
|
2064
|
+
|
|
2065
|
+
if (sent) {
|
|
2066
|
+
// RENDER THE MERCHANT'S success message \u2014 do not hardcode.
|
|
2067
|
+
return <div>{schema.successMessage}</div>;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
return (
|
|
2071
|
+
<form onSubmit={onSubmit} noValidate>
|
|
2072
|
+
<h1>{schema.name}</h1>
|
|
2073
|
+
{schema.description && <p>{schema.description}</p>}
|
|
2074
|
+
|
|
2075
|
+
{/* Honeypot \u2014 must be visually hidden from humans, not from bots */}
|
|
2076
|
+
<div aria-hidden style={{ position: 'absolute', left: '-10000px', width: 0, height: 0, overflow: 'hidden' }}>
|
|
2077
|
+
<label htmlFor="contact-honeypot">Leave this field empty</label>
|
|
2078
|
+
<input id="contact-honeypot" type="text" tabIndex={-1} autoComplete="off"
|
|
2079
|
+
value={honeypot} onChange={(e) => setHoneypot(e.target.value)} />
|
|
2080
|
+
</div>
|
|
2081
|
+
|
|
2082
|
+
{schema.fields.map((field) => (
|
|
2083
|
+
<DynamicField key={field.key} field={field}
|
|
2084
|
+
value={values[field.key] ?? defaultValueFor(field)}
|
|
2085
|
+
onChange={(v) => setValues((p) => ({ ...p, [field.key]: v }))} />
|
|
2086
|
+
))}
|
|
2087
|
+
|
|
2088
|
+
<button type="submit" disabled={loading}>
|
|
2089
|
+
{loading ? '\u2026' : schema.submitButton}
|
|
2090
|
+
</button>
|
|
2091
|
+
</form>
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2094
|
+
\`\`\`
|
|
2095
|
+
|
|
2096
|
+
### Rules
|
|
2097
|
+
|
|
2098
|
+
- **Rate limit:** 3 submissions / 60s per IP \u2014 show a friendly "try again later" message on HTTP 429.
|
|
2099
|
+
- **Honeypot:** always render a hidden field named \`honeypot\` and **never send** it. Bots fill every input; the server rejects submissions carrying a non-empty \`honeypot\`.
|
|
2100
|
+
- **Built-in keys:** \`name\`, \`email\`, \`phone\`, \`subject\`, \`message\` always exist on the default form; the legacy \`createInquiry\` shape keeps working forever.
|
|
2101
|
+
- **Max values:** each field value is capped at 10 000 chars server-side. Validate client-side before submitting using \`field.validation.maxLength\`.
|
|
2102
|
+
- **Required fields:** respect \`field.isRequired\`. The server validates too, but show \`required\` on the input for browser-level UX.
|
|
2103
|
+
- **Validation:** when \`field.validation.pattern\` is present, pass it as the input's \`pattern\` attribute; the server also enforces it. Likewise \`minLength\`/\`maxLength\`/\`min\`/\`max\`.
|
|
2104
|
+
- **Enum values:** SELECT and MULTI_SELECT always return \`enumValues\` (non-empty array). Render from \`enumValues\`, never a hardcoded list.
|
|
2105
|
+
- **Visibility:** the server strips fields with \`isVisible=false\`, so \`schema.fields\` only contains things to render.
|
|
2106
|
+
- **Origin check:** the endpoint validates \`Origin\` against the store's allowed origins. Test from your real storefront URL, not \`file://\` or localhost unless the merchant added it to allowed origins.
|
|
2107
|
+
- **Locale handling:** always pass \`locale\` to \`contactForms.get()\` and to \`createInquiry()\`. The staff inbox filters inquiries by language. Omit to fall back to the store's default language.
|
|
2108
|
+
- **Success message:** render \`schema.successMessage\` as-is. (Variable interpolation like \`{{customerName}}\` is not yet wired; any such placeholders currently render literally. Safe to use plain text.)
|
|
2109
|
+
- **Caching:** the schema GET is safe to cache per \`{storeId, formKey, locale}\`. 60 s feels live without hammering the API.
|
|
2110
|
+
|
|
2111
|
+
### Multiple forms
|
|
2112
|
+
|
|
2113
|
+
If the merchant set up more than one form (e.g. a \`main\` form on \`/contact\`
|
|
2114
|
+
and a \`newsletter\` form embedded in the footer), render each form from its
|
|
2115
|
+
own \`contactForms.get(key, locale)\` call. The \`formKey\` you pass to
|
|
2116
|
+
\`createInquiry\` must match.`;
|
|
2117
|
+
}
|
|
1624
2118
|
function getSectionByTopic(topic, connectionId, currency) {
|
|
1625
2119
|
const cid = connectionId || "vc_YOUR_CONNECTION_ID";
|
|
1626
2120
|
const cur = currency || "USD";
|
|
@@ -1647,6 +2141,8 @@ function getSectionByTopic(topic, connectionId, currency) {
|
|
|
1647
2141
|
return getDiscountsSection();
|
|
1648
2142
|
case "recommendations":
|
|
1649
2143
|
return getRecommendationsSection();
|
|
2144
|
+
case "product-customization-fields":
|
|
2145
|
+
return getProductCustomizationFieldsSection();
|
|
1650
2146
|
case "tax":
|
|
1651
2147
|
return getTaxDisplaySection(cur);
|
|
1652
2148
|
case "i18n":
|
|
@@ -1657,6 +2153,8 @@ function getSectionByTopic(topic, connectionId, currency) {
|
|
|
1657
2153
|
return getTypeQuickReference();
|
|
1658
2154
|
case "admin":
|
|
1659
2155
|
return getAdminApiSection();
|
|
2156
|
+
case "inquiries":
|
|
2157
|
+
return getContactInquiriesSection();
|
|
1660
2158
|
case "all":
|
|
1661
2159
|
return [
|
|
1662
2160
|
"# Brainerce SDK \u2014 full topic dump",
|
|
@@ -1721,6 +2219,10 @@ function getSectionByTopic(topic, connectionId, currency) {
|
|
|
1721
2219
|
"",
|
|
1722
2220
|
"---",
|
|
1723
2221
|
"",
|
|
2222
|
+
getProductCustomizationFieldsSection(),
|
|
2223
|
+
"",
|
|
2224
|
+
"---",
|
|
2225
|
+
"",
|
|
1724
2226
|
getTaxDisplaySection(cur),
|
|
1725
2227
|
"",
|
|
1726
2228
|
"---",
|
|
@@ -1729,10 +2231,14 @@ function getSectionByTopic(topic, connectionId, currency) {
|
|
|
1729
2231
|
"",
|
|
1730
2232
|
"---",
|
|
1731
2233
|
"",
|
|
1732
|
-
getAdminApiSection()
|
|
2234
|
+
getAdminApiSection(),
|
|
2235
|
+
"",
|
|
2236
|
+
"---",
|
|
2237
|
+
"",
|
|
2238
|
+
getContactInquiriesSection()
|
|
1733
2239
|
].join("\n");
|
|
1734
2240
|
default:
|
|
1735
|
-
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`;
|
|
2241
|
+
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, inquiries, all`;
|
|
1736
2242
|
}
|
|
1737
2243
|
}
|
|
1738
2244
|
|
|
@@ -1752,11 +2258,13 @@ var GET_SDK_DOCS_SCHEMA = {
|
|
|
1752
2258
|
"inventory",
|
|
1753
2259
|
"discounts",
|
|
1754
2260
|
"recommendations",
|
|
2261
|
+
"product-customization-fields",
|
|
1755
2262
|
"tax",
|
|
1756
2263
|
"critical-rules",
|
|
1757
2264
|
"type-reference",
|
|
1758
2265
|
"i18n",
|
|
1759
2266
|
"admin",
|
|
2267
|
+
"inquiries",
|
|
1760
2268
|
"all"
|
|
1761
2269
|
]).describe("The SDK documentation topic to retrieve"),
|
|
1762
2270
|
connectionId: z.string().optional().describe("Vibe-coded connection ID (starts with vc_). Used to personalize setup code."),
|
|
@@ -1796,6 +2304,7 @@ interface Product {
|
|
|
1796
2304
|
brands?: Array<{ id: string; name: string }>;
|
|
1797
2305
|
tags?: string[];
|
|
1798
2306
|
metafields?: ProductMetafield[];
|
|
2307
|
+
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.
|
|
1799
2308
|
productAttributeOptions?: Array<{
|
|
1800
2309
|
id: string;
|
|
1801
2310
|
attributeId: string;
|
|
@@ -1854,6 +2363,32 @@ interface ProductMetafield {
|
|
|
1854
2363
|
variantId?: string | null;
|
|
1855
2364
|
}
|
|
1856
2365
|
|
|
2366
|
+
// Metafield type enum \u2014 used by ProductCustomizationField.type
|
|
2367
|
+
type MetafieldType =
|
|
2368
|
+
| 'TEXT' | 'TEXTAREA' | 'NUMBER' | 'BOOLEAN' | 'DATE' | 'DATETIME'
|
|
2369
|
+
| 'URL' | 'COLOR' | 'DIMENSION' | 'WEIGHT' | 'JSON'
|
|
2370
|
+
| 'IMAGE' | 'GALLERY'
|
|
2371
|
+
| 'SELECT' | 'MULTI_SELECT';
|
|
2372
|
+
|
|
2373
|
+
// Customer-facing input assigned per product. Render on the PDP in the order of \`position\`.
|
|
2374
|
+
// Submit values keyed by \`key\` inside AddToCartDto.metadata.
|
|
2375
|
+
// Upload files via \`uploadCustomizationFile()\` first, then place the returned \`url\` in metadata.
|
|
2376
|
+
interface ProductCustomizationField {
|
|
2377
|
+
definitionId: string;
|
|
2378
|
+
name: string; // Label to show buyers
|
|
2379
|
+
key: string; // Use as metadata key in AddToCartDto.metadata
|
|
2380
|
+
description?: string | null; // Help text
|
|
2381
|
+
type: MetafieldType;
|
|
2382
|
+
required: boolean;
|
|
2383
|
+
minLength?: number | null; // TEXT/TEXTAREA char bounds; MULTI_SELECT array bounds
|
|
2384
|
+
maxLength?: number | null;
|
|
2385
|
+
minValue?: number | null; // NUMBER bounds
|
|
2386
|
+
maxValue?: number | null;
|
|
2387
|
+
enumValues?: string[]; // REQUIRED for SELECT / MULTI_SELECT
|
|
2388
|
+
defaultValue?: string | null;
|
|
2389
|
+
position: number; // Render order
|
|
2390
|
+
}
|
|
2391
|
+
|
|
1857
2392
|
interface ProductQueryParams {
|
|
1858
2393
|
page?: number;
|
|
1859
2394
|
limit?: number;
|
|
@@ -1937,6 +2472,7 @@ interface CartItem {
|
|
|
1937
2472
|
sku?: string | null;
|
|
1938
2473
|
image?: ProductImage | string | null;
|
|
1939
2474
|
} | null;
|
|
2475
|
+
metadata?: Record<string, unknown> | null; // Buyer-submitted customization values, keyed by ProductCustomizationField.key
|
|
1940
2476
|
createdAt: string;
|
|
1941
2477
|
updatedAt: string;
|
|
1942
2478
|
}
|
|
@@ -2001,6 +2537,9 @@ interface Checkout {
|
|
|
2001
2537
|
taxAmount: string;
|
|
2002
2538
|
taxBreakdown?: TaxBreakdown | null;
|
|
2003
2539
|
total: string;
|
|
2540
|
+
surchargeAmount: string;
|
|
2541
|
+
appliedSurcharges?: Array<{ key: string; name: string; value: unknown; amount: string }> | null;
|
|
2542
|
+
customFieldValues?: Record<string, unknown> | null;
|
|
2004
2543
|
couponCode?: string | null;
|
|
2005
2544
|
lineItems: CheckoutLineItem[]; // Use THIS for order summary, NOT cart.items!
|
|
2006
2545
|
itemCount: number;
|
|
@@ -2019,6 +2558,7 @@ interface CheckoutLineItem {
|
|
|
2019
2558
|
discountAmount: string;
|
|
2020
2559
|
product: { id: string; name: string; sku: string; images?: ProductImage[]; };
|
|
2021
2560
|
variant?: { id: string; name?: string | null; sku?: string | null; image?: ProductImage | string | null; } | null;
|
|
2561
|
+
metadata?: Record<string, unknown> | null; // Copied from CartItem.metadata \u2014 buyer customization values
|
|
2022
2562
|
}
|
|
2023
2563
|
|
|
2024
2564
|
interface CheckoutAddress {
|
|
@@ -2100,6 +2640,27 @@ interface Order {
|
|
|
2100
2640
|
billingAddress?: OrderAddress;
|
|
2101
2641
|
hasDownloads?: boolean;
|
|
2102
2642
|
createdAt: string;
|
|
2643
|
+
|
|
2644
|
+
// Payment + fulfillment
|
|
2645
|
+
paymentMethod?: string | null;
|
|
2646
|
+
financialStatus?: string | null; // "pending" | "paid" | "refunded" | "partially_refunded" | "voided"
|
|
2647
|
+
fulfillmentStatus?: string | null; // "unfulfilled" | "partial" | "fulfilled"
|
|
2648
|
+
|
|
2649
|
+
// Tracking
|
|
2650
|
+
trackingNumber?: string | null;
|
|
2651
|
+
trackingUrl?: string | null;
|
|
2652
|
+
carrier?: string | null;
|
|
2653
|
+
shippedAt?: string | null; // ISO-8601
|
|
2654
|
+
deliveredAt?: string | null; // ISO-8601
|
|
2655
|
+
|
|
2656
|
+
// Timeline of status transitions, chronological
|
|
2657
|
+
statusHistory?: OrderStatusChange[] | null;
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
interface OrderStatusChange {
|
|
2661
|
+
status: OrderStatus;
|
|
2662
|
+
at: string; // ISO-8601
|
|
2663
|
+
note?: string | null;
|
|
2103
2664
|
}
|
|
2104
2665
|
|
|
2105
2666
|
// \u26A0\uFE0F OrderItem is FLAT \u2014 unlike CartItem which is NESTED
|
|
@@ -2113,6 +2674,9 @@ interface OrderItem {
|
|
|
2113
2674
|
unitPrice?: string; // alias
|
|
2114
2675
|
totalPrice?: string;
|
|
2115
2676
|
image?: string; // FLAT: item.image (NOT nested)
|
|
2677
|
+
// Snapshot of buyer-submitted customization values captured at checkout.
|
|
2678
|
+
// Keyed by metafield slug. \`value\` is string[] for MULTI_SELECT / GALLERY, string otherwise.
|
|
2679
|
+
customizations?: Record<string, { label: string; value: string | string[]; type: string }>;
|
|
2116
2680
|
}
|
|
2117
2681
|
|
|
2118
2682
|
interface OrderCustomer {
|
|
@@ -2334,7 +2898,129 @@ interface ProductRecommendationsResponse {
|
|
|
2334
2898
|
}
|
|
2335
2899
|
interface CartRecommendationsResponse {
|
|
2336
2900
|
recommendations: ProductRecommendation[];
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
// Cart include types (for consolidated getCart requests)
|
|
2904
|
+
type CartIncludeOption = 'recommendations' | 'upgrades' | 'bundles';
|
|
2905
|
+
interface CartIncludeOptions {
|
|
2906
|
+
include?: CartIncludeOption[];
|
|
2907
|
+
}
|
|
2908
|
+
interface CartWithIncludes extends Cart {
|
|
2909
|
+
recommendations?: { recommendations: ProductRecommendation[] };
|
|
2910
|
+
upgrades?: { upgrades: Record<string, CartUpgradeSuggestion> };
|
|
2911
|
+
bundles?: { bundles: CartBundleOffer[] };
|
|
2912
|
+
}
|
|
2913
|
+
interface CartUpgradeSuggestion {
|
|
2914
|
+
targetProduct: ProductRecommendation;
|
|
2915
|
+
priceDelta: string;
|
|
2916
|
+
deltaPercent: number;
|
|
2917
|
+
}
|
|
2918
|
+
interface CartBundleOffer {
|
|
2919
|
+
id: string;
|
|
2920
|
+
bundleProduct: ProductRecommendation;
|
|
2921
|
+
originalPrice: string;
|
|
2922
|
+
discountedPrice: string;
|
|
2923
|
+
discountType: 'PERCENTAGE' | 'FIXED_AMOUNT';
|
|
2924
|
+
discountValue: number;
|
|
2925
|
+
requiresVariantSelection: boolean;
|
|
2926
|
+
lockedVariant?: { id: string; name?: string };
|
|
2337
2927
|
}`;
|
|
2928
|
+
var INQUIRIES_TYPES = `// ---- Contact Inquiries & Forms ----
|
|
2929
|
+
|
|
2930
|
+
// Two shapes \u2014 legacy and flexible. The two may be mixed; \`fields\` wins on key collision.
|
|
2931
|
+
interface CreateInquiryInput {
|
|
2932
|
+
// Legacy shape (still supported forever, backed by default "main" form)
|
|
2933
|
+
name?: string; // max 120 chars (if provided)
|
|
2934
|
+
email?: string; // must be valid email if provided
|
|
2935
|
+
subject?: string; // max 200 chars
|
|
2936
|
+
message?: string; // max 10000 chars
|
|
2937
|
+
phone?: string;
|
|
2938
|
+
|
|
2939
|
+
// Flexible shape
|
|
2940
|
+
formKey?: string; // defaults to "main"; merchant-defined keys e.g. "newsletter"
|
|
2941
|
+
fields?: Record<string, unknown>; // bag of values keyed by field key (built-in or custom)
|
|
2942
|
+
locale?: string; // e.g. "en", "he" \u2014 stored on the inquiry
|
|
2943
|
+
sourceMetadata?: Record<string, unknown>; // arbitrary context (e.g. { page: '/contact', campaign: 'fall-2026' })
|
|
2944
|
+
|
|
2945
|
+
// Shared
|
|
2946
|
+
customerId?: string; // link to logged-in customer
|
|
2947
|
+
metadata?: Record<string, unknown>; // deprecated alias of sourceMetadata
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
interface CreateInquiryResponse {
|
|
2951
|
+
id: string;
|
|
2952
|
+
status: 'NEW';
|
|
2953
|
+
createdAt: string; // ISO datetime
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
// ---- Form schema (for dynamic rendering) ----
|
|
2957
|
+
|
|
2958
|
+
type ContactFormFieldType =
|
|
2959
|
+
| 'TEXT' | 'TEXTAREA' | 'EMAIL' | 'PHONE' | 'NUMBER'
|
|
2960
|
+
| 'SELECT' | 'MULTI_SELECT' | 'CHECKBOX' | 'URL' | 'DATE';
|
|
2961
|
+
|
|
2962
|
+
interface ContactFormFieldValidation {
|
|
2963
|
+
minLength?: number;
|
|
2964
|
+
maxLength?: number;
|
|
2965
|
+
min?: number;
|
|
2966
|
+
max?: number;
|
|
2967
|
+
pattern?: string; // regex string
|
|
2968
|
+
patternMessage?: string;
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
interface ContactFormPublicField {
|
|
2972
|
+
key: string; // stable identifier, e.g. "email", "company"
|
|
2973
|
+
type: ContactFormFieldType;
|
|
2974
|
+
label: string; // already localized for the requested locale
|
|
2975
|
+
placeholder?: string; // already localized
|
|
2976
|
+
helpText?: string; // already localized
|
|
2977
|
+
isRequired: boolean;
|
|
2978
|
+
enumValues?: { value: string; label: string }[]; // present (non-empty) for SELECT / MULTI_SELECT
|
|
2979
|
+
validation?: ContactFormFieldValidation;
|
|
2980
|
+
defaultValue?: string;
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
interface ContactFormPublic {
|
|
2984
|
+
id: string;
|
|
2985
|
+
key: string; // e.g. "main", "newsletter"
|
|
2986
|
+
name: string; // already localized \u2014 use as form heading
|
|
2987
|
+
description?: string; // already localized \u2014 use as subtitle
|
|
2988
|
+
submitButton: string; // already localized \u2014 use as submit label
|
|
2989
|
+
successMessage: string; // already localized \u2014 render after submit succeeds
|
|
2990
|
+
fields: ContactFormPublicField[]; // in display order; hidden fields already filtered out
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
interface ContactFormSummary {
|
|
2994
|
+
key: string;
|
|
2995
|
+
name: string;
|
|
2996
|
+
isDefault: boolean;
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
// Field-type \u2192 HTML mapping for dynamic rendering
|
|
3000
|
+
// ------------------------------------------------------------------
|
|
3001
|
+
// TEXT \u2192 <input type="text" ... pattern?={validation.pattern}>
|
|
3002
|
+
// TEXTAREA \u2192 <textarea rows={6} ...>
|
|
3003
|
+
// EMAIL \u2192 <input type="email" autoComplete="email" ...>
|
|
3004
|
+
// PHONE \u2192 <input type="tel" autoComplete="tel" ...>
|
|
3005
|
+
// NUMBER \u2192 <input type="number" min={validation.min} max={validation.max}>
|
|
3006
|
+
// URL \u2192 <input type="url" ...>
|
|
3007
|
+
// DATE \u2192 <input type="date" ...> (value is ISO yyyy-MM-dd)
|
|
3008
|
+
// SELECT \u2192 <select>{enumValues.map(...)}</select> \u2014 always rendered from enumValues
|
|
3009
|
+
// MULTI_SELECT \u2192 multiple <input type="checkbox">, value is string[]
|
|
3010
|
+
// CHECKBOX \u2192 single <input type="checkbox">, value is boolean
|
|
3011
|
+
// ------------------------------------------------------------------
|
|
3012
|
+
|
|
3013
|
+
// SDK methods
|
|
3014
|
+
// await brainerce.createInquiry(input) \u2192 POST /stores/{storeId}/inquiries
|
|
3015
|
+
// await brainerce.contactForms.list() \u2192 GET /stores/{storeId}/contact-forms
|
|
3016
|
+
// await brainerce.contactForms.get(key?, locale?) \u2192 GET /stores/{storeId}/contact-forms/{key}?locale={locale}
|
|
3017
|
+
//
|
|
3018
|
+
// Rules
|
|
3019
|
+
// - Rate limit: 3 submissions / 60s per IP \u2014 handle 429 responses gracefully
|
|
3020
|
+
// - Honeypot: always render an invisible field named \`honeypot\` and never send it
|
|
3021
|
+
// - Always pass \`locale\` \u2014 inbox filters inquiries by language, and schema labels come back translated
|
|
3022
|
+
// - Render \`schema.name\` / \`description\` / \`submitButton\` / \`successMessage\` directly \u2014 do NOT hardcode copy
|
|
3023
|
+
// - Unknown field keys are stripped server-side \u2014 safe to send extras during dev`;
|
|
2338
3024
|
var TYPES_BY_DOMAIN = {
|
|
2339
3025
|
products: PRODUCTS_TYPES,
|
|
2340
3026
|
cart: CART_TYPES,
|
|
@@ -2342,7 +3028,8 @@ var TYPES_BY_DOMAIN = {
|
|
|
2342
3028
|
orders: ORDERS_TYPES,
|
|
2343
3029
|
customers: CUSTOMERS_TYPES,
|
|
2344
3030
|
payments: PAYMENTS_TYPES,
|
|
2345
|
-
helpers: HELPERS_TYPES
|
|
3031
|
+
helpers: HELPERS_TYPES,
|
|
3032
|
+
inquiries: INQUIRIES_TYPES
|
|
2346
3033
|
};
|
|
2347
3034
|
function getTypesByDomain(domain) {
|
|
2348
3035
|
if (domain === "all") {
|
|
@@ -2363,7 +3050,17 @@ var AVAILABLE_DOMAINS = Object.keys(TYPES_BY_DOMAIN);
|
|
|
2363
3050
|
var GET_TYPE_DEFINITIONS_NAME = "get-type-definitions";
|
|
2364
3051
|
var GET_TYPE_DEFINITIONS_DESCRIPTION = "Get TypeScript type definitions from the Brainerce SDK, segmented by domain. Use this when you need to understand the exact shape of objects like Product, Cart, Checkout, Order, etc.";
|
|
2365
3052
|
var GET_TYPE_DEFINITIONS_SCHEMA = {
|
|
2366
|
-
domain: z2.enum([
|
|
3053
|
+
domain: z2.enum([
|
|
3054
|
+
"products",
|
|
3055
|
+
"cart",
|
|
3056
|
+
"checkout",
|
|
3057
|
+
"orders",
|
|
3058
|
+
"customers",
|
|
3059
|
+
"payments",
|
|
3060
|
+
"helpers",
|
|
3061
|
+
"inquiries",
|
|
3062
|
+
"all"
|
|
3063
|
+
]).describe(
|
|
2367
3064
|
'The domain of types to retrieve. Use "helpers" for helper function signatures and common types like StoreInfo, PaginatedResponse.'
|
|
2368
3065
|
)
|
|
2369
3066
|
};
|
|
@@ -2397,7 +3094,8 @@ var GET_CODE_EXAMPLE_SCHEMA = {
|
|
|
2397
3094
|
"coupon-apply-and-remove",
|
|
2398
3095
|
"reservation-countdown",
|
|
2399
3096
|
"search-autocomplete-debounce",
|
|
2400
|
-
"i18n-set-locale"
|
|
3097
|
+
"i18n-set-locale",
|
|
3098
|
+
"order-history-full"
|
|
2401
3099
|
]).describe("The SDK operation to get a snippet for.")
|
|
2402
3100
|
};
|
|
2403
3101
|
var SNIPPETS = {
|
|
@@ -2492,23 +3190,51 @@ const checkout = await client.getCheckout(checkoutId);
|
|
|
2492
3190
|
"checkout-payment-providers": `// Fetch configured payment providers for this checkout.
|
|
2493
3191
|
import { client } from './brainerce';
|
|
2494
3192
|
|
|
2495
|
-
const providers = await client.getPaymentProviders(
|
|
2496
|
-
//
|
|
2497
|
-
//
|
|
2498
|
-
//
|
|
2499
|
-
//
|
|
2500
|
-
//
|
|
2501
|
-
// '
|
|
2502
|
-
|
|
2503
|
-
//
|
|
2504
|
-
//
|
|
2505
|
-
|
|
3193
|
+
const { providers, hasPayments, defaultProvider } = await client.getPaymentProviders();
|
|
3194
|
+
// each provider: { id, provider, name, publicKey, supportedMethods, testMode, isDefault, clientSdk? }
|
|
3195
|
+
//
|
|
3196
|
+
// After createPaymentIntent, the response carries a clientSdk.renderType that
|
|
3197
|
+
// tells your UI how to render the payment step. The 5 possible values:
|
|
3198
|
+
//
|
|
3199
|
+
// 'sdk-widget' \u2014 Load the provider's JS SDK (scriptUrl), then mount
|
|
3200
|
+
// into a <div id={containerId}>. Used by Stripe,
|
|
3201
|
+
// PayPal, Grow. The SDK paints its own form inside
|
|
3202
|
+
// your DOM. You control layout/placement.
|
|
3203
|
+
//
|
|
3204
|
+
// 'iframe' \u2014 Render <iframe src={clientSecret}>. Two flavors:
|
|
3205
|
+
// a) URL path contains "/embed/" \u2192 Brainerce-hosted
|
|
3206
|
+
// embed page. Render INLINE in your checkout
|
|
3207
|
+
// flow. Listen for postMessage:
|
|
3208
|
+
// - 'brainerce:resize' \u2192 update iframe height
|
|
3209
|
+
// - 'brainerce:redirect' \u2192 validated top-level
|
|
3210
|
+
// navigation (e.g. Bit express-pay)
|
|
3211
|
+
// b) Any other URL \u2192 provider-hosted page. Render
|
|
3212
|
+
// inside a modal overlay (it carries its own
|
|
3213
|
+
// branding/chrome).
|
|
3214
|
+
//
|
|
3215
|
+
// 'redirect' \u2014 Full-page navigate: window.location.href = URL.
|
|
3216
|
+
// Customer leaves your site, pays on provider page,
|
|
3217
|
+
// returns via SuccessRedirectUrl.
|
|
3218
|
+
//
|
|
3219
|
+
// 'sandbox' \u2014 Test mode. Render a "Complete Test Order" button;
|
|
3220
|
+
// call completeGuestCheckout(checkoutId). Orders are
|
|
3221
|
+
// flagged isTestOrder: true. No real charge.
|
|
3222
|
+
//
|
|
3223
|
+
// 'embedded-fields' \u2014 (Reserved \u2014 not yet shipped in any provider.) PCI
|
|
3224
|
+
// micro-iframes for sensitive fields (card number,
|
|
3225
|
+
// CVV) mounted directly into the merchant form. Full
|
|
3226
|
+
// control of surrounding layout, like Stripe
|
|
3227
|
+
// Elements. Requires the provider to ship a client
|
|
3228
|
+
// SDK that exposes mount points.
|
|
3229
|
+
//
|
|
3230
|
+
// Always branch on clientSdk.renderType \u2014 NEVER hard-code by provider name.
|
|
3231
|
+
// New providers can be added without storefront changes.
|
|
2506
3232
|
|
|
2507
|
-
// If providers is empty, NO payment provider is configured and customers
|
|
2508
|
-
// cannot pay. Display a clear error \u2014 do not try to fake a checkout.
|
|
2509
3233
|
if (providers.length === 0) {
|
|
2510
3234
|
throw new Error('No payment provider configured for this store');
|
|
2511
|
-
}
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
const active = providers[0];`,
|
|
2512
3238
|
"checkout-stripe-confirm": `// Stripe confirm flow. Requires @stripe/stripe-js and @stripe/react-stripe-js
|
|
2513
3239
|
// (or the vanilla Stripe.js browser SDK).
|
|
2514
3240
|
import { loadStripe } from '@stripe/stripe-js';
|
|
@@ -2722,12 +3448,82 @@ const locale = 'he'; // e.g., from the /[locale] route segment
|
|
|
2722
3448
|
client.setLocale(locale);
|
|
2723
3449
|
|
|
2724
3450
|
// ALL content comes back translated: products, categories, brands,
|
|
2725
|
-
// tags, variants
|
|
2726
|
-
//
|
|
3451
|
+
// tags, variants (name + attributes keys/values), metafields,
|
|
3452
|
+
// cart items, checkout line items, recommendations, bundles,
|
|
3453
|
+
// order bumps, search suggestions, discount banners, nudges,
|
|
2727
3454
|
// badges, order history.
|
|
2728
3455
|
|
|
2729
3456
|
// For RTL locales (he, ar), set the document direction.
|
|
2730
|
-
// Framework-neutral: document.documentElement.setAttribute('dir', 'rtl')
|
|
3457
|
+
// Framework-neutral: document.documentElement.setAttribute('dir', 'rtl');`,
|
|
3458
|
+
"order-history-full": `// Full "My Orders" account page. Render every piece of data the server
|
|
3459
|
+
// already returns \u2014 not just order number / total / status.
|
|
3460
|
+
//
|
|
3461
|
+
// Data you get from client.getMyOrders() (already on the wire today):
|
|
3462
|
+
// - items[].customizations \u2014 buyer's custom-field entries
|
|
3463
|
+
// - statusHistory \u2014 status timeline
|
|
3464
|
+
// - shippingAddress \u2014 name + address lines + country
|
|
3465
|
+
// - trackingNumber / trackingUrl / carrier / shippedAt / deliveredAt
|
|
3466
|
+
// - paymentMethod / financialStatus / fulfillmentStatus
|
|
3467
|
+
// - appliedDiscounts \u2014 shaped buyer-facing view
|
|
3468
|
+
// - hasDownloads \u2014 call /orders/:id/downloads to fetch
|
|
3469
|
+
//
|
|
3470
|
+
// Do NOT render: accountId, storeId, customerId, notes, customFieldValues
|
|
3471
|
+
// (merchant-internal, order-level), appliedRuleIds, appliedSurcharges,
|
|
3472
|
+
// surchargeAmount, downloadMeta (raw), pickupLocationData. The backend
|
|
3473
|
+
// does not return these to buyers \u2014 if you see them in a type, they're
|
|
3474
|
+
// an oversight; still skip them.
|
|
3475
|
+
import { client } from './brainerce';
|
|
3476
|
+
import type { Order, OrderItem, OrderStatusChange } from 'brainerce';
|
|
3477
|
+
|
|
3478
|
+
const { data: orders } = await client.getMyOrders({ page: 1, limit: 10 });
|
|
3479
|
+
|
|
3480
|
+
for (const order of orders) {
|
|
3481
|
+
// Line items + per-item customizations
|
|
3482
|
+
for (const item of order.items) {
|
|
3483
|
+
// item.image, item.productName, item.quantity, item.price
|
|
3484
|
+
if (item.customizations) {
|
|
3485
|
+
for (const [fieldId, entry] of Object.entries(item.customizations)) {
|
|
3486
|
+
// entry = { label, value, type }
|
|
3487
|
+
// Type-aware render (see account-page section in get-sdk-docs):
|
|
3488
|
+
// TEXT / TEXTAREA / URL / NUMBER / SELECT \u2014 plain text
|
|
3489
|
+
// BOOLEAN \u2014 \u2713 / \u2717
|
|
3490
|
+
// MULTI_SELECT \u2014 comma-separated
|
|
3491
|
+
// IMAGE \u2014 <img src={value}>
|
|
3492
|
+
// GALLERY \u2014 wrap grid of <img>
|
|
3493
|
+
// COLOR \u2014 swatch + hex
|
|
3494
|
+
// DATE / DATETIME \u2014 localized format
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
// Status timeline \u2014 skip silently when null/empty
|
|
3500
|
+
if (order.statusHistory?.length) {
|
|
3501
|
+
for (const entry of order.statusHistory as OrderStatusChange[]) {
|
|
3502
|
+
// entry = { status, at, note? }
|
|
3503
|
+
// Render a dot + localized status label + new Date(entry.at).toLocaleString()
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
// Shipping + tracking
|
|
3508
|
+
if (order.shippingAddress) {
|
|
3509
|
+
// Render firstName lastName \xB7 line1 \xB7 line2 \xB7 city, region \xB7 postalCode \xB7 country
|
|
3510
|
+
}
|
|
3511
|
+
if (order.trackingNumber) {
|
|
3512
|
+
// Render carrier \xB7 trackingNumber, shippedAt/deliveredAt dates,
|
|
3513
|
+
// and an anchor to order.trackingUrl when present.
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3516
|
+
// Payment
|
|
3517
|
+
if (order.paymentMethod || order.financialStatus) {
|
|
3518
|
+
// paymentMethod: 'card' | 'paypal' | 'bank_transfer' | 'cash_on_delivery' | ...
|
|
3519
|
+
// financialStatus: 'pending' | 'paid' | 'refunded' | 'partially_refunded' | 'voided'
|
|
3520
|
+
// Map financialStatus \u2192 a colored badge.
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
// All sections above are conditional. Absent data = section not rendered \u2014
|
|
3525
|
+
// never show empty placeholders. The create-brainerce-store template's
|
|
3526
|
+
// src/components/account/order-history.tsx is the reference implementation.`
|
|
2731
3527
|
};
|
|
2732
3528
|
async function handleGetCodeExample(args) {
|
|
2733
3529
|
const snippet = SNIPPETS[args.operation];
|
|
@@ -3193,7 +3989,7 @@ var RULES = {
|
|
|
3193
3989
|
- All prices are STRINGS in the SDK. \`parseFloat\` them before math or comparisons.
|
|
3194
3990
|
- CartItem and CheckoutLineItem are NESTED (\`item.product.name\`, \`item.unitPrice\`). OrderItem is FLAT (\`item.name\`, \`item.price\`). They are not interchangeable.
|
|
3195
3991
|
- Cart has no \`.total\` field \u2014 call \`getCartTotals(cart)\` to get \`{ subtotal, tax, shipping, discount, total }\`.
|
|
3196
|
-
- \`smartGetCart()\` returns
|
|
3992
|
+
- \`smartGetCart()\` returns \`CartWithIncludes\` (extends \`Cart\`). Pass \`{ include: ['recommendations', 'upgrades', 'bundles'] }\` to fetch extras in one request.
|
|
3197
3993
|
- \`startGuestCheckout()\` is also a discriminated union. Check \`result.tracked\` before reading \`result.checkoutId\`.`
|
|
3198
3994
|
}
|
|
3199
3995
|
};
|
|
@@ -3231,6 +4027,7 @@ var GET_BUSINESS_FLOWS_SCHEMA = {
|
|
|
3231
4027
|
"order-confirmation",
|
|
3232
4028
|
"cart-persistence",
|
|
3233
4029
|
"inventory-reservation",
|
|
4030
|
+
"product-customization",
|
|
3234
4031
|
"all"
|
|
3235
4032
|
]).describe('Which flow to retrieve. Use "all" to get every flow.')
|
|
3236
4033
|
};
|
|
@@ -3350,7 +4147,7 @@ Build the OAuth button region AND the callback handler even when no providers ar
|
|
|
3350
4147
|
- **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).
|
|
3351
4148
|
- **Writes:** use \`client.addToCart\`, \`client.updateCartItem\`, \`client.removeCartItem\`, \`client.applyCoupon\`, \`client.removeCoupon\`. After each mutation the SDK returns the updated cart.
|
|
3352
4149
|
- **Totals:** call \`getCartTotals(cart)\` \u2014 do NOT read \`cart.total\`. The helper understands taxes, shipping, and discounts.
|
|
3353
|
-
- **\`smartGetCart()\`** returns \`
|
|
4150
|
+
- **\`smartGetCart()\`** returns \`CartWithIncludes\` (extends \`Cart\`). All carts are server-side. Pass \`{ include: ['recommendations', 'upgrades', 'bundles'] }\` to fetch extras in one request.
|
|
3354
4151
|
- **NEVER mutate cart state outside SDK helpers.** Any hand-rolled cart update risks desync with the reservation timer and the checkout flow.`
|
|
3355
4152
|
},
|
|
3356
4153
|
"inventory-reservation": {
|
|
@@ -3364,6 +4161,54 @@ Build the OAuth button region AND the callback handler even when no providers ar
|
|
|
3364
4161
|
- **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.
|
|
3365
4162
|
|
|
3366
4163
|
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.`
|
|
4164
|
+
},
|
|
4165
|
+
"product-customization": {
|
|
4166
|
+
title: "Product customization (buyer input)",
|
|
4167
|
+
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.
|
|
4168
|
+
|
|
4169
|
+
1. **Read the fields** from the product payload. Sort by \`position\`. If the array is empty, nothing to render.
|
|
4170
|
+
2. **Render one control per field** based on \`type\`:
|
|
4171
|
+
- \`TEXT\` / \`URL\` / \`COLOR\` / \`DIMENSION\` / \`WEIGHT\` \u2192 \`<input type="text">\` (honour \`minLength\`/\`maxLength\`)
|
|
4172
|
+
- \`TEXTAREA\` \u2192 \`<textarea>\`
|
|
4173
|
+
- \`NUMBER\` \u2192 \`<input type="number">\` (honour \`minValue\`/\`maxValue\`)
|
|
4174
|
+
- \`BOOLEAN\` \u2192 checkbox (value \`true\`/\`false\`)
|
|
4175
|
+
- \`DATE\` \u2192 \`<input type="date">\` (ISO \`YYYY-MM-DD\`)
|
|
4176
|
+
- \`DATETIME\` \u2192 \`<input type="datetime-local">\` (ISO timestamp)
|
|
4177
|
+
- \`SELECT\` \u2192 \`<select>\` populated from \`enumValues\` (value = one string)
|
|
4178
|
+
- \`MULTI_SELECT\` \u2192 checkbox group from \`enumValues\` (value = \`string[]\`)
|
|
4179
|
+
- \`IMAGE\` \u2192 file input + preview (value = one URL string)
|
|
4180
|
+
- \`GALLERY\` \u2192 multi-file input (value = \`string[]\` of URLs)
|
|
4181
|
+
- \`JSON\` \u2192 advanced; render an admin-style editor or skip unless you control the data
|
|
4182
|
+
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.
|
|
4183
|
+
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.
|
|
4184
|
+
5. **Add to cart with metadata keyed by \`field.key\`:**
|
|
4185
|
+
\`\`\`ts
|
|
4186
|
+
await client.addToCart({
|
|
4187
|
+
productId,
|
|
4188
|
+
quantity: 1,
|
|
4189
|
+
metadata: {
|
|
4190
|
+
engraving_text: 'For Mom',
|
|
4191
|
+
frame_color: 'Gold',
|
|
4192
|
+
upload_photo: photoUrl, // from uploadCustomizationFile()
|
|
4193
|
+
addons: ['Gift wrap'], // MULTI_SELECT
|
|
4194
|
+
},
|
|
4195
|
+
});
|
|
4196
|
+
\`\`\`
|
|
4197
|
+
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).
|
|
4198
|
+
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.
|
|
4199
|
+
|
|
4200
|
+
**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.
|
|
4201
|
+
|
|
4202
|
+
Server-side guardrails that WILL reject bad requests (so validate client-side to avoid round-trips):
|
|
4203
|
+
- Unknown keys \u2192 rejected. Only use keys that appear in \`product.customizationFields\`.
|
|
4204
|
+
- Missing \`required\` fields \u2192 rejected.
|
|
4205
|
+
- \`SELECT\` value not in \`enumValues\` \u2192 rejected.
|
|
4206
|
+
- \`MULTI_SELECT\` value not \`string[]\` OR containing values outside \`enumValues\` \u2192 rejected.
|
|
4207
|
+
- \`IMAGE\` / \`GALLERY\` values must be URLs returned from \`/customization-upload\` on this store \u2014 pasting an external URL is rejected.
|
|
4208
|
+
- Upload > 5MB or non-image MIME \u2192 rejected by upload endpoint.
|
|
4209
|
+
- More than 10 uploads per IP per minute \u2192 429.
|
|
4210
|
+
|
|
4211
|
+
Never render customization fields without also wiring the upload + metadata flow \u2014 a form that submits nothing is worse than no form at all.`
|
|
3367
4212
|
}
|
|
3368
4213
|
};
|
|
3369
4214
|
var FLOW_ORDER = [
|
|
@@ -3374,7 +4219,8 @@ var FLOW_ORDER = [
|
|
|
3374
4219
|
"oauth",
|
|
3375
4220
|
"order-confirmation",
|
|
3376
4221
|
"cart-persistence",
|
|
3377
|
-
"inventory-reservation"
|
|
4222
|
+
"inventory-reservation",
|
|
4223
|
+
"product-customization"
|
|
3378
4224
|
];
|
|
3379
4225
|
async function handleGetBusinessFlows(args) {
|
|
3380
4226
|
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.";
|
|
@@ -3439,6 +4285,14 @@ var FEATURES = [
|
|
|
3439
4285
|
sdk: "client.getProductBySlug(slug). Helpers: getProductPriceInfo, getVariantPrice, getStockStatus, getVariantOptions, getProductSwatches, getDescriptionContent, getProductMetafieldValue.",
|
|
3440
4286
|
mandatory: "mandatory"
|
|
3441
4287
|
},
|
|
4288
|
+
{
|
|
4289
|
+
id: "product-customization-fields",
|
|
4290
|
+
title: "Render buyer-input customization fields on the product page",
|
|
4291
|
+
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.",
|
|
4292
|
+
sdk: "product.customizationFields, client.uploadCustomizationFile(file), client.addToCart({ productId, quantity, metadata })",
|
|
4293
|
+
flowRef: "product-customization",
|
|
4294
|
+
mandatory: "mandatory"
|
|
4295
|
+
},
|
|
3442
4296
|
{
|
|
3443
4297
|
id: "cart",
|
|
3444
4298
|
title: "Manage a cart with quantity, removal, and totals",
|