@brainerce/mcp-server 3.5.0 → 3.6.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/stdio.js CHANGED
@@ -93,7 +93,7 @@ npm install brainerce
93
93
  import { BrainerceClient } from 'brainerce';
94
94
 
95
95
  export const client = new BrainerceClient({
96
- connectionId: '${connectionId}',
96
+ salesChannelId: '${connectionId}',
97
97
  });
98
98
 
99
99
  // Cart helpers \u2014 save cart ID to localStorage
@@ -195,7 +195,7 @@ async function startCheckout() {
195
195
  ### Full Checkout Flow (Multi-Provider)
196
196
 
197
197
  1. Customer fills cart
198
- 2. Customer optionally applies coupon (cart page)
198
+ 2. Customer optionally applies coupon on cart page \u2192 \`applyCoupon(cartId, code)\`
199
199
  3. Detect payment providers \u2192 \`getPaymentProviders()\`
200
200
  4. Start checkout session (\`startGuestCheckout()\` or \`createCheckout()\`)
201
201
  5. Set shipping address (includes required email) \u2192 \`setShippingAddress()\`
@@ -213,7 +213,7 @@ import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-
213
213
 
214
214
  function CheckoutPage() {
215
215
  const [paymentData, setPaymentData] = useState<{
216
- clientSecret: string; provider: string; checkoutId: string;
216
+ clientSecret: string; provider: string; checkoutId: string; clientSdk?: { renderType: string };
217
217
  } | null>(null);
218
218
  const [stripePromise, setStripePromise] = useState<ReturnType<typeof loadStripe> | null>(null);
219
219
  const [paypalClientId, setPaypalClientId] = useState<string | null>(null);
@@ -275,7 +275,7 @@ function CheckoutPage() {
275
275
  // saveCard: customerOptedIn, // optional opt-in
276
276
  });
277
277
 
278
- setPaymentData({ clientSecret: paymentIntent.clientSecret, provider: paymentIntent.provider, checkoutId });
278
+ setPaymentData({ clientSecret: paymentIntent.clientSecret, provider: paymentIntent.provider, checkoutId, clientSdk: paymentIntent.clientSdk });
279
279
 
280
280
  // Step 5: Initialize the correct payment provider
281
281
  if (paymentIntent.provider === 'stripe' && stripeProvider) {
@@ -885,6 +885,10 @@ function ProductPage({ product, storeInfo }: { product: Product; storeInfo: Stor
885
885
  ? getVariantPrice(selectedVariant, product.basePrice).toString()
886
886
  : product.salePrice || product.basePrice;
887
887
 
888
+ // \u2705 For catalog cards: use pre-computed priceMin/priceMax instead of iterating variants
889
+ // product.priceMin / product.priceMax are always set for VARIABLE products with explicit variant prices.
890
+ // product.priceVaries is true when the range should be shown as "\u20AA49 \u2013 \u20AA199".
891
+
888
892
  // Build attribute buttons (size, color, etc.)
889
893
  const allOptions = product.variants?.map(v => getVariantOptions(v)) || [];
890
894
  const attrNames = [...new Set(allOptions.flatMap(opts => opts.map(o => o.name)))];
@@ -1099,6 +1103,7 @@ const total = cart.total;
1099
1103
 
1100
1104
  ### Coupons
1101
1105
 
1106
+ **On the cart page** (before checkout is created):
1102
1107
  \`\`\`typescript
1103
1108
  // Apply coupon \u2014 returns updated Cart with discountAmount
1104
1109
  const updatedCart = await client.applyCoupon(cartId, 'SAVE20');
@@ -1106,13 +1111,24 @@ console.log(updatedCart.discountAmount); // "10.00"
1106
1111
  console.log(updatedCart.couponCode); // "SAVE20"
1107
1112
 
1108
1113
  // Remove coupon
1109
- const updatedCart = await client.removeCoupon(cartId);
1114
+ await client.removeCoupon(cartId);
1110
1115
 
1111
1116
  // Calculate totals including discount
1112
1117
  const totals = getCartTotals(cart); // { subtotal, discount, shipping, total }
1113
1118
  \`\`\`
1114
1119
 
1115
- Add a coupon code input on the cart page. Coupons only work on server \`Cart\` (has \`id\` field). Checkout inherits the coupon \u2014 show \`checkout.discountAmount\` and \`checkout.couponCode\` in checkout order summary.
1120
+ **On the checkout page** (after checkout session exists \u2014 ALWAYS use this when checkoutId is available):
1121
+ \`\`\`typescript
1122
+ // Applies to cart AND updates checkout totals in one call
1123
+ const checkout = await client.applyCheckoutCoupon(checkoutId, 'SAVE20');
1124
+ console.log(checkout.discountAmount); // "10.00"
1125
+ console.log(checkout.total); // correctly updated total
1126
+
1127
+ // Remove coupon from checkout
1128
+ await client.removeCheckoutCoupon(checkoutId);
1129
+ \`\`\`
1130
+
1131
+ > \u26A0\uFE0F **Critical:** if a checkout session already exists, ALWAYS use \`applyCheckoutCoupon(checkoutId, code)\`. Using \`applyCoupon(cartId, code)\` after checkout creation does NOT update the checkout total \u2014 payment will charge the original amount. Show \`checkout.discountAmount\` and \`checkout.couponCode\` in the order summary.
1116
1132
 
1117
1133
  ### \u26A0\uFE0F Checkout Order Summary \u2014 Use checkout.lineItems, NOT cart.items!
1118
1134
 
@@ -1537,6 +1553,53 @@ The merchant can hide a group entirely for one variant by setting \`maxOverride:
1537
1553
 
1538
1554
  Allergens (informational chips), scheduled availability windows, nested combos (depth \u2264 3 via \`nestedByModifierId\`), and downsell modifiers (negative \`priceDelta\`) are all built on the same data model. See INTEGRATION-OPTIONAL.md "Restaurant / build-your-own products" for those flows.`;
1539
1555
  }
1556
+ function getProductReviewsSection() {
1557
+ return `## Product Reviews (\u2605 ratings + JSON-LD for SEO)
1558
+
1559
+ Reviews publish immediately on submit. Merchants hide individual reviews via the admin surface \u2014 no PENDING queue.
1560
+
1561
+ \`\`\`typescript
1562
+ // PDP \u2014 list visible reviews
1563
+ const { data, meta } = await client.listProductReviews(productId, { page: 1, limit: 20 });
1564
+ data.forEach(r => render(r.authorName, r.rating, r.body, r.verifiedPurchase));
1565
+
1566
+ // PDP \u2014 submit form
1567
+ await client.submitProductReview(productId, {
1568
+ authorName, authorEmail, rating: 5, body,
1569
+ });
1570
+ \`\`\`
1571
+
1572
+ Each \`Product\` carries denormalized rollups: \`product.avgRating\`, \`product.reviewCount\`. Use these on PLP cards.
1573
+
1574
+ ### JSON-LD (mandatory for SEO stars in Google)
1575
+
1576
+ Emit \`aggregateRating\` ONLY when \`product.reviewCount > 0\`:
1577
+
1578
+ \`\`\`typescript
1579
+ const productJsonLd = {
1580
+ '@context': 'https://schema.org',
1581
+ '@type': 'Product',
1582
+ name: product.name,
1583
+ // \u2026existing fields
1584
+ ...(product.reviewCount > 0 && {
1585
+ aggregateRating: {
1586
+ '@type': 'AggregateRating',
1587
+ ratingValue: product.avgRating,
1588
+ reviewCount: product.reviewCount,
1589
+ bestRating: 5,
1590
+ worstRating: 1,
1591
+ },
1592
+ }),
1593
+ };
1594
+ \`\`\`
1595
+
1596
+ ### Rules
1597
+ - Rating must be an integer 1-5 (DB constraint).
1598
+ - Duplicate from same email \u2192 409 Conflict.
1599
+ - Submit endpoint is throttled to 3 / 60s / IP \u2014 surface a friendly "please wait" on 429.
1600
+ - \`verifiedPurchase\` is derived server-side from DELIVERED orders \u2014 DO NOT try to set it.
1601
+ - Stores with \`reviewsEnabled = false\` return 403 on storefront endpoints.`;
1602
+ }
1540
1603
  function getInventorySection() {
1541
1604
  return `## Inventory, Stock Display & Reservation Countdown
1542
1605
 
@@ -1932,7 +1995,7 @@ subsequent SDK call sends the \`Accept-Language\` header automatically:
1932
1995
  \`\`\`typescript
1933
1996
  import { BrainerceClient } from 'brainerce';
1934
1997
 
1935
- const client = new BrainerceClient({ connectionId: 'vc_...' });
1998
+ const client = new BrainerceClient({ salesChannelId: 'vc_...' });
1936
1999
  client.setLocale('he'); // done \u2014 all calls are now in Hebrew
1937
2000
  \`\`\`
1938
2001
 
@@ -2483,6 +2546,8 @@ function getSectionByTopic(topic, connectionId, currency) {
2483
2546
  return getDiscountsSection();
2484
2547
  case "recommendations":
2485
2548
  return getRecommendationsSection();
2549
+ case "reviews":
2550
+ return getProductReviewsSection();
2486
2551
  case "product-customization-fields":
2487
2552
  return getProductCustomizationFieldsSection();
2488
2553
  case "modifier-groups":
@@ -2555,6 +2620,10 @@ function getSectionByTopic(topic, connectionId, currency) {
2555
2620
  "",
2556
2621
  "---",
2557
2622
  "",
2623
+ getProductReviewsSection(),
2624
+ "",
2625
+ "---",
2626
+ "",
2558
2627
  getDiscountsSection(),
2559
2628
  "",
2560
2629
  "---",
@@ -2606,6 +2675,7 @@ var GET_SDK_DOCS_SCHEMA = {
2606
2675
  "inventory",
2607
2676
  "discounts",
2608
2677
  "recommendations",
2678
+ "reviews",
2609
2679
  "product-customization-fields",
2610
2680
  "tax",
2611
2681
  "critical-rules",
@@ -2649,6 +2719,9 @@ interface Product {
2649
2719
  basePrice: string; // Use parseFloat() for calculations
2650
2720
  salePrice?: string | null;
2651
2721
  costPrice?: string | null;
2722
+ priceMin?: string | null; // Lowest variant price (VARIABLE products). Use for catalog/JSON-LD range display.
2723
+ priceMax?: string | null; // Highest variant price (VARIABLE products).
2724
+ priceVaries?: boolean; // true when variant prices differ \u2014 show "\u20AA49 \u2013 \u20AA199" range.
2652
2725
  status: string;
2653
2726
  type: 'SIMPLE' | 'VARIABLE';
2654
2727
  isDownloadable?: boolean;
@@ -3219,9 +3292,9 @@ var HELPERS_TYPES = `// ---- Helper Functions (import from 'brainerce') ----
3219
3292
  // Price helpers
3220
3293
  function formatPrice(priceString: string | number | undefined | null, options?: { currency?: string; locale?: string; }): string;
3221
3294
  function getProductPrice(product: Pick<Product, 'basePrice' | 'salePrice'>): number;
3222
- function getProductPriceInfo(product: Pick<Product, 'basePrice' | 'salePrice'>): {
3295
+ function getProductPriceInfo(product: Pick<Product, 'basePrice' | 'salePrice' | 'discount' | 'priceMin' | 'priceVaries'>): {
3223
3296
  price: number; originalPrice: number; isOnSale: boolean; discountAmount: number; discountPercent: number;
3224
- };
3297
+ }; // Falls back to priceMin when basePrice=0 (VARIABLE products)
3225
3298
  function getVariantPrice(variant: Pick<ProductVariant, 'price' | 'salePrice'>, productBasePrice: string): number;
3226
3299
 
3227
3300
  // Cart helpers
@@ -3334,6 +3407,37 @@ interface CartBundleOffer {
3334
3407
  totalOriginalPrice: string;
3335
3408
  totalDiscountedPrice: string;
3336
3409
  }`;
3410
+ var REVIEWS_TYPES = `// ---- Product Reviews ----
3411
+
3412
+ interface ProductReview {
3413
+ id: string;
3414
+ productId: string;
3415
+ authorName: string; // max 100 chars
3416
+ rating: number; // integer 1-5
3417
+ body: string | null; // plain text, max 5000 chars
3418
+ verifiedPurchase: boolean; // derived server-side from DELIVERED orders
3419
+ hiddenAt?: string | null; // ISO datetime; admin responses only \u2014 null = visible
3420
+ createdAt: string; // ISO datetime
3421
+ }
3422
+
3423
+ interface ProductReviewAdmin extends ProductReview {
3424
+ customerId: string | null;
3425
+ authorEmail: string | null;
3426
+ orderId: string | null;
3427
+ updatedAt: string;
3428
+ }
3429
+
3430
+ interface SubmitProductReviewInput {
3431
+ authorName: string;
3432
+ authorEmail?: string; // recommended \u2014 used for guest dedupe + verified-purchase derivation
3433
+ rating: number; // 1-5
3434
+ body?: string; // optional
3435
+ }
3436
+
3437
+ // Each Product carries denormalized review stats:
3438
+ // product.avgRating: number // 0 when reviewCount is 0
3439
+ // product.reviewCount: number // count of visible (hiddenAt IS NULL) reviews
3440
+ `;
3337
3441
  var INQUIRIES_TYPES = `// ---- Contact Inquiries & Forms ----
3338
3442
 
3339
3443
  // Two shapes \u2014 legacy and flexible. The two may be mixed; \`fields\` wins on key collision.
@@ -3577,6 +3681,7 @@ var TYPES_BY_DOMAIN = {
3577
3681
  payments: PAYMENTS_TYPES,
3578
3682
  helpers: HELPERS_TYPES,
3579
3683
  inquiries: INQUIRIES_TYPES,
3684
+ reviews: REVIEWS_TYPES,
3580
3685
  "modifier-groups": MODIFIER_GROUPS_TYPES
3581
3686
  };
3582
3687
  function getTypesByDomain(domain) {
@@ -3607,6 +3712,7 @@ var GET_TYPE_DEFINITIONS_SCHEMA = {
3607
3712
  "payments",
3608
3713
  "helpers",
3609
3714
  "inquiries",
3715
+ "reviews",
3610
3716
  "all"
3611
3717
  ]).describe(
3612
3718
  'The domain of types to retrieve. Use "helpers" for helper function signatures and common types like StoreInfo, PaginatedResponse.'
@@ -3723,7 +3829,7 @@ function selectVariant(product: Product, selections: Record<string, string>) {
3723
3829
  import { client } from './brainerce';
3724
3830
 
3725
3831
  // 1. Submit the address + email. Email is REQUIRED on the DTO.
3726
- const { shippingRates } = await client.setShippingAddress(checkoutId, {
3832
+ const { checkout, rates } = await client.setShippingAddress(checkoutId, {
3727
3833
  email,
3728
3834
  firstName,
3729
3835
  lastName,
@@ -3734,10 +3840,11 @@ const { shippingRates } = await client.setShippingAddress(checkoutId, {
3734
3840
  postalCode,
3735
3841
  country,
3736
3842
  });
3843
+ // rates = available shipping rates; checkout = updated checkout object
3737
3844
 
3738
3845
  // 2. Let the customer pick one of the returned rates.
3739
- const chosen = shippingRates[0]; // user selection
3740
- await client.setShippingMethod(checkoutId, chosen.id);
3846
+ const chosen = rates[0]; // user selection
3847
+ await client.selectShippingMethod(checkoutId, chosen.id);
3741
3848
 
3742
3849
  // 3. Re-fetch the checkout to get updated totals (tax may change after address).
3743
3850
  const checkout = await client.getCheckout(checkoutId);
@@ -3791,21 +3898,21 @@ if (providers.length === 0) {
3791
3898
  }
3792
3899
 
3793
3900
  const active = providers[0];`,
3794
- "checkout-stripe-confirm": `// Stripe confirm flow. Requires @stripe/stripe-js and @stripe/react-stripe-js
3795
- // (or the vanilla Stripe.js browser SDK).
3901
+ "checkout-stripe-confirm": `// Stripe confirm flow. Requires @stripe/stripe-js.
3796
3902
  import { loadStripe } from '@stripe/stripe-js';
3797
3903
  import { client } from './brainerce';
3798
3904
 
3799
3905
  const stripe = await loadStripe(stripePublicKey);
3800
3906
  if (!stripe) throw new Error('Stripe failed to load');
3801
3907
 
3802
- // The SDK returns a clientSecret via the provider's config after setPaymentMethod.
3803
- const { clientSecret } = await client.setPaymentMethod(checkoutId, {
3804
- providerId: stripeProvider.id,
3908
+ // Create a payment intent \u2014 returns clientSecret for Stripe Elements.
3909
+ const intent = await client.createPaymentIntent(checkoutId, {
3910
+ successUrl: \`\${window.location.origin}/order-confirmation?checkoutId=\${checkoutId}\`,
3911
+ cancelUrl: \`\${window.location.origin}/checkout?error=cancelled\`,
3805
3912
  });
3806
3913
 
3807
- // Use Stripe Elements to collect the card, then confirm:
3808
- const result = await stripe.confirmCardPayment(clientSecret, {
3914
+ // Use Stripe Elements to confirm:
3915
+ const result = await stripe.confirmCardPayment(intent.clientSecret, {
3809
3916
  payment_method: { card: cardElement },
3810
3917
  });
3811
3918
 
@@ -3814,21 +3921,21 @@ if (result.error) {
3814
3921
  return;
3815
3922
  }
3816
3923
 
3817
- // Success \u2014 redirect to your confirmation page with checkoutId in the URL.
3924
+ // Success \u2014 redirect to confirmation page.
3818
3925
  // The confirmation page runs handlePaymentSuccess + waitForOrder.
3819
3926
  window.location.assign(\`/order-confirmation?checkoutId=\${checkoutId}\`);`,
3820
3927
  "checkout-paypal-confirm": `// PayPal confirm flow. Use the PayPal JS SDK (render a PayPal button component).
3821
3928
  import { client } from './brainerce';
3822
3929
 
3823
- // When the PayPal button's onApprove fires with the PayPal order ID:
3824
- async function onPayPalApprove(paypalOrderId: string) {
3825
- await client.setPaymentMethod(checkoutId, {
3826
- providerId: paypalProvider.id,
3827
- providerData: { paypalOrderId },
3828
- });
3930
+ // Create a payment intent first \u2014 for PayPal this returns the PayPal order ID.
3931
+ const intent = await client.createPaymentIntent(checkoutId, {
3932
+ successUrl: \`\${window.location.origin}/order-confirmation?checkoutId=\${checkoutId}\`,
3933
+ cancelUrl: \`\${window.location.origin}/checkout?error=cancelled\`,
3934
+ });
3829
3935
 
3830
- // PayPal confirmation \u2014 the SDK captures the order server-side.
3831
- // After this resolves, redirect to confirmation.
3936
+ // Render a PayPal button using intent.clientSdk (provider SDK config).
3937
+ // When the PayPal button's onApprove fires, redirect to confirmation:
3938
+ function onPayPalApprove() {
3832
3939
  window.location.assign(\`/order-confirmation?checkoutId=\${checkoutId}\`);
3833
3940
  }`,
3834
3941
  "checkout-sandbox-confirm": `// Sandbox payment \u2014 store has sandboxPaymentsEnabled=true. No real charge.
@@ -3871,7 +3978,7 @@ async function registerAndMaybeVerify(input: {
3871
3978
  firstName: string;
3872
3979
  lastName: string;
3873
3980
  }) {
3874
- const result = await client.register(input);
3981
+ const result = await client.registerCustomer(input);
3875
3982
  if (result.requiresVerification) {
3876
3983
  // Route the user to your verify-email UI. Do NOT treat them as logged in.
3877
3984
  return { state: 'needs-verification' as const };
@@ -3892,7 +3999,7 @@ async function resendCode() {
3892
3999
  import { client } from './brainerce';
3893
4000
 
3894
4001
  async function login(email: string, password: string) {
3895
- const result = await client.login({ email, password });
4002
+ const result = await client.loginCustomer(email, password);
3896
4003
  if (result.requiresVerification) {
3897
4004
  return { state: 'needs-verification' as const };
3898
4005
  }
@@ -3927,25 +4034,37 @@ async function submitReset(token: string, newPassword: string) {
3927
4034
  "oauth-redirect-and-callback": `// OAuth sign-in. Redirect flow, NOT popup.
3928
4035
  import { client } from './brainerce';
3929
4036
 
3930
- // On your login / register UI:
3931
- const providers = await client.getAvailableOAuthProviders();
3932
- // Render one button per provider. On click, redirect the browser:
3933
- for (const p of providers) {
3934
- // button.onclick = () => window.location.assign(p.authorizationUrl)
4037
+ // Step 1: Get available provider names (strings: 'GOOGLE', 'FACEBOOK', 'GITHUB')
4038
+ const { providers } = await client.getAvailableOAuthProviders();
4039
+
4040
+ // Step 2: For each provider, fetch the authorization URL, then redirect.
4041
+ // Call this when the user clicks a provider button:
4042
+ async function redirectToOAuth(provider: string) {
4043
+ const { authorizationUrl } = await client.getOAuthAuthorizeUrl(provider, {
4044
+ redirectUrl: \`\${window.location.origin}/auth/callback\`,
4045
+ });
4046
+ window.location.href = authorizationUrl; // full-page redirect, NOT a popup
3935
4047
  }
3936
4048
 
3937
- // On your OAuth callback route (SERVER-SIDE / BFF):
3938
- // 1. Read token + oauth_success (or oauth_error) from query params.
3939
- // 2. If oauth_error: redirect to login with the error message.
3940
- // 3. If oauth_success: exchange the token for a session cookie (your BFF),
3941
- // then redirect the browser to /account or wherever.
3942
- // Do NOT allow the token to land in browser history as a query param.`,
4049
+ // Step 3: On your /auth/callback route, read token from URL params:
4050
+ const params = new URLSearchParams(window.location.search);
4051
+ const oauthError = params.get('oauth_error');
4052
+ const token = params.get('token');
4053
+ if (oauthError) {
4054
+ window.location.href = '/login?error=' + encodeURIComponent(oauthError);
4055
+ } else if (token) {
4056
+ client.setCustomerToken(token);
4057
+ window.location.href = '/account';
4058
+ }`,
3943
4059
  "coupon-apply-and-remove": `// Coupon input. Build the UI even if no coupons exist today \u2014 it auto-hides.
4060
+ // IMPORTANT: use applyCheckoutCoupon when a checkoutId is available (checkout page).
4061
+ // Using applyCoupon after checkout creation does NOT update the checkout total.
3944
4062
  import { client } from './brainerce';
3945
4063
 
3946
- async function applyCoupon(code: string) {
4064
+ // On cart page (no checkout session yet)
4065
+ async function applyCouponToCart(cartId: string, code: string) {
3947
4066
  try {
3948
- const cart = await client.applyCoupon(code);
4067
+ const cart = await client.applyCoupon(cartId, code);
3949
4068
  return { ok: true, cart };
3950
4069
  } catch (err) {
3951
4070
  // Invalid / expired / minimum not met. Surface the specific error.
@@ -3953,17 +4072,30 @@ async function applyCoupon(code: string) {
3953
4072
  }
3954
4073
  }
3955
4074
 
3956
- async function removeCoupon() {
3957
- const cart = await client.removeCoupon();
3958
- return cart;
4075
+ async function removeCouponFromCart(cartId: string) {
4076
+ return client.removeCoupon(cartId);
4077
+ }
4078
+
4079
+ // On checkout page (checkout session already exists \u2014 always prefer this)
4080
+ async function applyCouponToCheckout(checkoutId: string, code: string) {
4081
+ try {
4082
+ const checkout = await client.applyCheckoutCoupon(checkoutId, code);
4083
+ return { ok: true, checkout }; // checkout.total is already updated
4084
+ } catch (err) {
4085
+ return { ok: false, error: (err as Error).message };
4086
+ }
4087
+ }
4088
+
4089
+ async function removeCouponFromCheckout(checkoutId: string) {
4090
+ return client.removeCheckoutCoupon(checkoutId);
3959
4091
  }`,
3960
4092
  "reservation-countdown": `// Reservation countdown. Use the SDK's expiry timestamp \u2014 do NOT invent
3961
4093
  // your own timer state.
3962
4094
  import { client } from './brainerce';
3963
4095
 
3964
- function getRemainingMs(cart: { reservationExpiresAt?: string | null }): number {
3965
- if (!cart.reservationExpiresAt) return 0;
3966
- const expiry = new Date(cart.reservationExpiresAt).getTime();
4096
+ function getRemainingMs(cart: { reservation?: { expiresAt?: string } }): number {
4097
+ if (!cart.reservation?.expiresAt) return 0;
4098
+ const expiry = new Date(cart.reservation.expiresAt).getTime();
3967
4099
  return Math.max(0, expiry - Date.now());
3968
4100
  }
3969
4101
 
@@ -3990,8 +4122,8 @@ function onSearchChange(query: string, onResults: (items: unknown[]) => void) {
3990
4122
  return;
3991
4123
  }
3992
4124
  searchTimer = setTimeout(async () => {
3993
- const results = await client.searchProducts(query);
3994
- onResults(results);
4125
+ const { products } = await client.getSearchSuggestions(query);
4126
+ onResults(products);
3995
4127
  }, 300);
3996
4128
  }`,
3997
4129
  "i18n-set-locale": `// i18n \u2014 only if get-store-capabilities reports i18n.enabled.
@@ -4348,19 +4480,13 @@ import { client } from './brainerce-client';
4348
4480
 
4349
4481
  interface CheckoutOptions {
4350
4482
  cartId: string;
4351
- shippingAddress: object;
4352
- shippingMethodId: string;
4353
- paymentProviderId: string;
4354
4483
  }
4355
4484
 
4356
4485
  async function createCheckoutSafe(opts: CheckoutOptions) {
4357
4486
  try {
4358
- return await client.createCheckout({
4359
- cartId: opts.cartId,
4360
- shippingAddress: opts.shippingAddress,
4361
- shippingMethodId: opts.shippingMethodId,
4362
- paymentProviderId: opts.paymentProviderId,
4363
- });
4487
+ // createCheckout only takes cartId (+ optional customerId, selectedItemIds).
4488
+ // Shipping address, shipping method, and payment are set via separate calls.
4489
+ return await client.createCheckout({ cartId: opts.cartId });
4364
4490
  } catch (err: unknown) {
4365
4491
  if (!isApiError(err, 'PRICE_DRIFT')) throw err;
4366
4492
 
@@ -4490,6 +4616,93 @@ function renderCartItem(item: CartItem): string {
4490
4616
  // multiply unitPrice \xD7 quantity directly.
4491
4617
  function lineTotal(item: CartItem): number {
4492
4618
  return item.unitPrice * item.quantity;
4619
+ }`,
4620
+ "product-reviews": `// PDP: list visible reviews + submit form + JSON-LD aggregateRating
4621
+ import { client } from '@/lib/brainerce';
4622
+ import type { Product, ProductReview, SubmitProductReviewInput } from 'brainerce';
4623
+
4624
+ export async function ProductReviewsSection({ product }: { product: Product }) {
4625
+ const { data } = await client.listProductReviews(product.id, { page: 1, limit: 20 });
4626
+
4627
+ return (
4628
+ <section>
4629
+ <h2>Reviews ({product.reviewCount ?? 0})</h2>
4630
+ {data.length === 0 && <p>No reviews yet \u2014 be the first.</p>}
4631
+ {data.map((r) => (
4632
+ <article key={r.id}>
4633
+ <strong>{r.authorName}</strong>
4634
+ {r.verifiedPurchase && <span> \xB7 Verified</span>}
4635
+ <div>{'\u2605'.repeat(r.rating)}{'\u2606'.repeat(5 - r.rating)}</div>
4636
+ {r.body && <p>{r.body}</p>}
4637
+ </article>
4638
+ ))}
4639
+ <ReviewForm productId={product.id} />
4640
+ </section>
4641
+ );
4642
+ }
4643
+
4644
+ // JSON-LD \u2014 emit aggregateRating only when there is data
4645
+ function productJsonLd(product: Product, url: string) {
4646
+ return {
4647
+ '@context': 'https://schema.org',
4648
+ '@type': 'Product',
4649
+ name: product.name,
4650
+ image: product.images?.[0]?.url,
4651
+ url,
4652
+ sku: product.sku ?? product.id,
4653
+ ...(product.reviewCount && product.reviewCount > 0 && {
4654
+ aggregateRating: {
4655
+ '@type': 'AggregateRating',
4656
+ ratingValue: product.avgRating,
4657
+ reviewCount: product.reviewCount,
4658
+ bestRating: 5,
4659
+ worstRating: 1,
4660
+ },
4661
+ }),
4662
+ };
4663
+ }
4664
+
4665
+ // Client-side form
4666
+ 'use client';
4667
+ import { useState } from 'react';
4668
+
4669
+ function ReviewForm({ productId }: { productId: string }) {
4670
+ const [rating, setRating] = useState(5);
4671
+ const [body, setBody] = useState('');
4672
+ const [name, setName] = useState('');
4673
+ const [email, setEmail] = useState('');
4674
+ const [error, setError] = useState<string | null>(null);
4675
+
4676
+ async function submit(e: React.FormEvent) {
4677
+ e.preventDefault();
4678
+ setError(null);
4679
+ try {
4680
+ await client.submitProductReview(productId, {
4681
+ authorName: name,
4682
+ authorEmail: email,
4683
+ rating,
4684
+ body: body || undefined,
4685
+ });
4686
+ window.location.reload();
4687
+ } catch (err: any) {
4688
+ if (err?.status === 409) setError("You've already reviewed this product.");
4689
+ else if (err?.status === 429) setError('Too many submissions. Please wait a moment.');
4690
+ else setError('Could not submit review. Please try again.');
4691
+ }
4692
+ }
4693
+
4694
+ return (
4695
+ <form onSubmit={submit}>
4696
+ <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Your name" required maxLength={100} />
4697
+ <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
4698
+ <select value={rating} onChange={(e) => setRating(Number(e.target.value))}>
4699
+ {[5,4,3,2,1].map(n => <option key={n} value={n}>{'\u2605'.repeat(n)}</option>)}
4700
+ </select>
4701
+ <textarea value={body} onChange={(e) => setBody(e.target.value)} maxLength={5000} placeholder="Tell us what you think (optional)" />
4702
+ <button type="submit">Submit review</button>
4703
+ {error && <p role="alert">{error}</p>}
4704
+ </form>
4705
+ );
4493
4706
  }`
4494
4707
  };
4495
4708
  async function handleGetCodeExample(args) {
@@ -5035,21 +5248,21 @@ var FLOWS = {
5035
5248
  1. **Collect address + email.** Build a form that captures customer email, billing address, shipping address (with \`line1\`, \`line2\`, \`city\`, \`region\`, \`postalCode\`, \`country\`). \`email\` is REQUIRED on \`SetShippingAddressDto\`.
5036
5249
  2. **Submit the address to get shipping rates.**
5037
5250
  \`\`\`ts
5038
- const { shippingRates } = await client.setShippingAddress(checkoutId, {
5251
+ const { checkout, rates } = await client.setShippingAddress(checkoutId, {
5039
5252
  email,
5040
5253
  firstName,
5041
5254
  lastName,
5042
5255
  line1, line2, city, region, postalCode, country,
5043
5256
  });
5044
5257
  \`\`\`
5045
- The response includes the available shipping rates for that address + the store's zones.
5258
+ \`rates\` = available shipping rates for that address + the store's zones. \`checkout\` = updated checkout object.
5046
5259
  3. **Let the customer pick a rate**, then persist the selection:
5047
5260
  \`\`\`ts
5048
- await client.setShippingMethod(checkoutId, rateId);
5261
+ await client.selectShippingMethod(checkoutId, rateId);
5049
5262
  \`\`\`
5050
- 4. **Fetch payment providers for this checkout:**
5263
+ 4. **Fetch payment providers:**
5051
5264
  \`\`\`ts
5052
- const providers = await client.getPaymentProviders(checkoutId);
5265
+ const providers = await client.getPaymentProviders();
5053
5266
  \`\`\`
5054
5267
  The response tells you which providers are configured (Stripe, Grow, PayPal, Sandbox) and how to render each. Each provider has a \`renderType\` telling you whether to show a Stripe Elements form, a redirect button, a PayPal button, or a sandbox "complete test order" button.
5055
5268
  5. **Confirm payment using the provider's recommended flow.** For Stripe: Stripe Elements \u2192 \`stripe.confirmCardPayment\` using the clientSecret returned by the SDK. For sandbox payments: call \`completeGuestCheckout(checkoutId)\` directly. For PayPal/Grow: follow the redirect and handle the return on your confirmation page.
@@ -5065,28 +5278,28 @@ Never bypass these steps. Never call \`submitGuestOrder\` / \`createOrder\` \u20
5065
5278
  "auth-register": {
5066
5279
  title: "Registration",
5067
5280
  body: `1. **Collect** email, password, first name, last name. Enforce strong passwords client-side: 8+ chars, upper, lower, number, special.
5068
- 2. **Call register:**
5281
+ 2. **Call registerCustomer:**
5069
5282
  \`\`\`ts
5070
- const result = await client.register({ email, password, firstName, lastName });
5283
+ const result = await client.registerCustomer({ email, password, firstName, lastName });
5071
5284
  \`\`\`
5072
5285
  3. **Branch on \`result.requiresVerification\`:**
5073
- - If \`true\`: route the user to your verify-email UI. Do NOT treat them as logged in yet.
5074
- - If \`false\`: the user is logged in. Store the token via your BFF session and route to the account area.
5286
+ - If \`true\`: store the token temporarily (e.g. sessionStorage), route the user to your verify-email UI. Do NOT treat them as logged in yet.
5287
+ - If \`false\`: call \`client.setCustomerToken(result.token)\` and route to the account area.
5075
5288
  4. **On the verify-email step:** collect a 6-digit code and call \`client.verifyEmail(code)\`. Offer a "resend code" button wired to \`client.resendVerificationEmail()\`.
5076
- 5. **After verifyEmail resolves:** the user is now logged in. Persist the session via your BFF and route to the account area.
5289
+ 5. **After verifyEmail resolves:** call \`client.setCustomerToken(result.token)\` \u2014 the user is now logged in \u2014 then route to the account area.
5077
5290
 
5078
5291
  Build the verify-email step EVEN IF the store currently has verification disabled. It auto-hides; store owners enable it later.`
5079
5292
  },
5080
5293
  "auth-login": {
5081
5294
  title: "Login",
5082
5295
  body: `1. **Collect** email + password.
5083
- 2. **Call login:**
5296
+ 2. **Call loginCustomer:**
5084
5297
  \`\`\`ts
5085
- const result = await client.login({ email, password });
5298
+ const result = await client.loginCustomer(email, password);
5086
5299
  \`\`\`
5087
5300
  3. **Branch on \`result.requiresVerification\`:**
5088
5301
  - If \`true\`: route to verify-email. The user must complete verification before accessing account features.
5089
- - If \`false\`: persist the session via your BFF and route to the previous page (or account area).
5302
+ - If \`false\`: call \`client.setCustomerToken(result.token)\` and route to the previous page (or account area).
5090
5303
  4. **Offer OAuth buttons** from \`client.getAvailableOAuthProviders()\`. Render a placeholder region even when no providers are returned \u2014 the region auto-hides today and shows buttons the moment a provider is enabled in the dashboard.
5091
5304
  5. **On error**, render the specific message (invalid credentials, rate limited, account disabled) \u2014 never swallow.`
5092
5305
  },
@@ -5111,15 +5324,24 @@ Build both steps EVEN IF the store has no email provider configured today \u2014
5111
5324
  },
5112
5325
  oauth: {
5113
5326
  title: "OAuth sign-in",
5114
- body: `1. **Render available providers:**
5327
+ body: `1. **Get available provider names:**
5115
5328
  \`\`\`ts
5116
- const providers = await client.getAvailableOAuthProviders();
5329
+ const { providers } = await client.getAvailableOAuthProviders();
5330
+ // providers = ['GOOGLE', 'FACEBOOK', 'GITHUB'] (strings, not objects with authorizationUrl)
5331
+ \`\`\`
5332
+ 2. **For each provider, fetch the authorization URL:**
5333
+ \`\`\`ts
5334
+ const { authorizationUrl } = await client.getOAuthAuthorizeUrl(provider, {
5335
+ redirectUrl: \`\${window.location.origin}/auth/callback\`,
5336
+ });
5337
+ window.location.href = authorizationUrl; // full-page redirect, NOT a popup
5117
5338
  \`\`\`
5118
- Each provider has \`{ provider: 'GOOGLE' | 'FACEBOOK' | 'GITHUB', authorizationUrl }\`.
5119
- 2. **Build a button for each** that redirects the browser to \`authorizationUrl\`. Do NOT open a popup; the full-page redirect is required.
5120
- 3. **On the provider's side** the user authenticates, and the provider redirects back to your OAuth callback URL with \`token\` + \`oauth_success\` (or \`oauth_error\`) query params.
5121
- 4. **The callback handler MUST run server-side (BFF).** Extract the token from the query params, exchange it for a session cookie via your BFF, then redirect the browser to the account area. Never let the token land in browser history as a query param or localStorage.
5122
- 5. **On \`oauth_error\`:** redirect to login with an error message.
5339
+ 3. **On the callback page** the URL contains \`token\` + \`oauth_success\` (or \`oauth_error\`) query params. Extract and apply the token:
5340
+ \`\`\`ts
5341
+ const token = new URLSearchParams(location.search).get('token');
5342
+ if (token) client.setCustomerToken(token); // then redirect to account
5343
+ \`\`\`
5344
+ 4. **On \`oauth_error\`:** redirect to login with an error message.
5123
5345
 
5124
5346
  Build the OAuth button region AND the callback handler even when no providers are configured. They auto-hide.`
5125
5347
  },
@@ -5141,7 +5363,7 @@ Build the OAuth button region AND the callback handler even when no providers ar
5141
5363
 
5142
5364
  - **Cart ID persistence:** the SDK stores the cart ID across reloads. You do not need to write cart-to-localStorage code yourself.
5143
5365
  - **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).
5144
- - **Writes:** use \`client.addToCart\`, \`client.updateCartItem\`, \`client.removeCartItem\`, \`client.applyCoupon\`, \`client.removeCoupon\`. After each mutation the SDK returns the updated cart.
5366
+ - **Writes:** use \`client.addToCart\`, \`client.updateCartItem\`, \`client.removeCartItem\`, \`client.applyCoupon\`, \`client.removeCoupon\`. After each mutation the SDK returns the updated cart. On the **checkout page** use \`client.applyCheckoutCoupon(checkoutId, code)\` / \`client.removeCheckoutCoupon(checkoutId)\` \u2014 these update checkout totals atomically. Never use \`applyCoupon\` after a checkout session exists.
5145
5367
  - **Totals:** call \`getCartTotals(cart)\` \u2014 do NOT read \`cart.total\`. The helper understands taxes, shipping, and discounts.
5146
5368
  - **\`smartGetCart()\`** returns \`CartWithIncludes\` (extends \`Cart\`). All carts are server-side. Pass \`{ include: ['recommendations', 'upgrades', 'bundles'] }\` to fetch extras in one request.
5147
5369
  - **NEVER mutate cart state outside SDK helpers.** Any hand-rolled cart update risks desync with the reservation timer and the checkout flow.`
@@ -5273,7 +5495,7 @@ var FEATURES = [
5273
5495
  id: "browse-products",
5274
5496
  title: "Browse, filter, and search products",
5275
5497
  description: "Users can list products with pagination, apply category / price / attribute / custom-field filters, sort by name / price / newest, and run a search with autocomplete. Custom-field filters surface metafield definitions whose filterable=true (SELECT, MULTI_SELECT, BOOLEAN). Must handle empty states and loading states.",
5276
- sdk: "client.getProducts({ page, limit, filters, sort, metafields }), client.getPublicMetafieldDefinitions(), client.searchProducts(query)",
5498
+ sdk: "client.getProducts({ page, limit, filters, sort, metafields }), client.getPublicMetafieldDefinitions(), client.getSearchSuggestions(query)",
5277
5499
  mandatory: "mandatory"
5278
5500
  },
5279
5501
  {
@@ -5283,6 +5505,13 @@ var FEATURES = [
5283
5505
  sdk: "client.getProductBySlug(slug). Helpers: getProductPriceInfo, getVariantPrice, getStockStatus, getVariantOptions, getProductSwatches, getDescriptionContent, getProductMetafieldValue.",
5284
5506
  mandatory: "mandatory"
5285
5507
  },
5508
+ {
5509
+ id: "product-reviews",
5510
+ title: "Display + collect product reviews with stars + JSON-LD",
5511
+ description: 'On the PDP show a Reviews section: list of customer reviews with star rating, author name, verified-purchase badge, body, and date. Include a submit form (name + email + 1-5 stars + optional body). On PLP cards show \u2605 avgRating and (reviewCount). Emit Product JSON-LD with aggregateRating ONLY when product.reviewCount > 0 \u2014 this is what unlocks the star rich snippet in Google. Reviews publish immediately; the merchant moderates from the admin surface. Rate-limited to 3 submits / 60s / IP. Show a friendly "you already reviewed this" message on 409.',
5512
+ sdk: "client.listProductReviews(productId), client.submitProductReview(productId, { authorName, authorEmail, rating, body }). Product.avgRating + Product.reviewCount are denormalized rollups returned on every product fetch.",
5513
+ mandatory: "mandatory"
5514
+ },
5286
5515
  {
5287
5516
  id: "product-customization-fields",
5288
5517
  title: "Render buyer-input customization fields on the product page",
@@ -5303,7 +5532,7 @@ var FEATURES = [
5303
5532
  id: "cart-coupons",
5304
5533
  title: "Apply and remove coupon codes",
5305
5534
  description: "Users can enter a coupon code, see it applied to the cart, see the discount amount, and remove it. Build the UI even if no coupons are configured today \u2014 it auto-hides.",
5306
- sdk: "client.applyCoupon(code), client.removeCoupon()",
5535
+ sdk: "client.applyCoupon(cartId, code), client.removeCoupon(cartId) \u2014 on cart page. client.applyCheckoutCoupon(checkoutId, code), client.removeCheckoutCoupon(checkoutId) \u2014 on checkout page (use when checkoutId exists)",
5307
5536
  mandatory: "mandatory",
5308
5537
  capabilityFlag: "hasCoupons",
5309
5538
  whenDisabledNote: "Store has no coupons configured today. Build the UI anyway \u2014 it auto-hides until the store owner creates a coupon."
@@ -5320,7 +5549,7 @@ var FEATURES = [
5320
5549
  id: "checkout",
5321
5550
  title: "Complete a full checkout end-to-end",
5322
5551
  description: "Users can enter their address, pick a shipping rate, pick a payment provider, pay, and land on a confirmation page showing the real order. Displays checkout.lineItems (not cart.items) on the summary.",
5323
- sdk: "client.setShippingAddress, client.setShippingMethod, client.getPaymentProviders, provider-specific confirm, client.handlePaymentSuccess, client.waitForOrder",
5552
+ sdk: "client.setShippingAddress (returns { checkout, rates }), client.selectShippingMethod, client.getPaymentProviders(), provider-specific confirm, client.handlePaymentSuccess, client.waitForOrder",
5324
5553
  flowRef: "checkout",
5325
5554
  mandatory: "mandatory"
5326
5555
  },
@@ -5336,7 +5565,7 @@ var FEATURES = [
5336
5565
  id: "register",
5337
5566
  title: "Register a new account with email verification",
5338
5567
  description: 'Users can create an account (email, password, first + last name). Handles the requiresVerification branch by routing to an email-verification step. Verification step collects a 6-digit code and has a "resend code" action.',
5339
- sdk: "client.register({email, password, firstName, lastName}), client.verifyEmail(code), client.resendVerificationEmail()",
5568
+ sdk: "client.registerCustomer({email, password, firstName, lastName}), client.verifyEmail(code), client.resendVerificationEmail()",
5340
5569
  flowRef: "auth-register",
5341
5570
  mandatory: "mandatory",
5342
5571
  capabilityFlag: "oauth",
@@ -5346,7 +5575,7 @@ var FEATURES = [
5346
5575
  id: "login",
5347
5576
  title: "Log in with email / password and handle verification branch",
5348
5577
  description: "Users can log in. The login form handles the requiresVerification branch by routing to the verify-email step. Specific errors render (bad credentials, rate limited, disabled account).",
5349
- sdk: "client.login({email, password})",
5578
+ sdk: "client.loginCustomer(email, password)",
5350
5579
  flowRef: "auth-login",
5351
5580
  mandatory: "mandatory"
5352
5581
  },
@@ -5379,14 +5608,14 @@ var FEATURES = [
5379
5608
  id: "header",
5380
5609
  title: "Global header with cart count and search",
5381
5610
  description: "A header visible across the experience with the store logo, navigation, a cart icon with live item count, a search input with autocomplete (debounced ~300ms, 2-char minimum), and a login / account action. Below the header, discount banners render when active.",
5382
- sdk: "client.getCart() for the count, client.searchProducts(query) for autocomplete",
5611
+ sdk: "client.getCart() for the count, client.getSearchSuggestions(query) for autocomplete",
5383
5612
  mandatory: "mandatory"
5384
5613
  },
5385
5614
  {
5386
5615
  id: "discount-banners",
5387
5616
  title: "Show active discount banners and badges",
5388
5617
  description: "Active discount rules render as banners (below the header) and as badges on product cards / detail pages. Build the UI even if the store has no active discounts today \u2014 it auto-hides.",
5389
- sdk: "client.getDiscountBanners(), getProductDiscountBadge(product)",
5618
+ sdk: "client.getDiscountBanners(), client.getProductDiscountBadge(productId)",
5390
5619
  mandatory: "mandatory",
5391
5620
  capabilityFlag: "hasDiscountRules",
5392
5621
  whenDisabledNote: "Store has no discount rules active. Build the banner and badge components anyway \u2014 they auto-hide."