@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/http.js +317 -88
- package/dist/bin/stdio.js +317 -88
- package/dist/index.js +317 -88
- package/dist/index.mjs +317 -88
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -119,7 +119,7 @@ npm install brainerce
|
|
|
119
119
|
import { BrainerceClient } from 'brainerce';
|
|
120
120
|
|
|
121
121
|
export const client = new BrainerceClient({
|
|
122
|
-
|
|
122
|
+
salesChannelId: '${connectionId}',
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
// Cart helpers \u2014 save cart ID to localStorage
|
|
@@ -221,7 +221,7 @@ async function startCheckout() {
|
|
|
221
221
|
### Full Checkout Flow (Multi-Provider)
|
|
222
222
|
|
|
223
223
|
1. Customer fills cart
|
|
224
|
-
2. Customer optionally applies coupon
|
|
224
|
+
2. Customer optionally applies coupon on cart page \u2192 \`applyCoupon(cartId, code)\`
|
|
225
225
|
3. Detect payment providers \u2192 \`getPaymentProviders()\`
|
|
226
226
|
4. Start checkout session (\`startGuestCheckout()\` or \`createCheckout()\`)
|
|
227
227
|
5. Set shipping address (includes required email) \u2192 \`setShippingAddress()\`
|
|
@@ -239,7 +239,7 @@ import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-
|
|
|
239
239
|
|
|
240
240
|
function CheckoutPage() {
|
|
241
241
|
const [paymentData, setPaymentData] = useState<{
|
|
242
|
-
clientSecret: string; provider: string; checkoutId: string;
|
|
242
|
+
clientSecret: string; provider: string; checkoutId: string; clientSdk?: { renderType: string };
|
|
243
243
|
} | null>(null);
|
|
244
244
|
const [stripePromise, setStripePromise] = useState<ReturnType<typeof loadStripe> | null>(null);
|
|
245
245
|
const [paypalClientId, setPaypalClientId] = useState<string | null>(null);
|
|
@@ -301,7 +301,7 @@ function CheckoutPage() {
|
|
|
301
301
|
// saveCard: customerOptedIn, // optional opt-in
|
|
302
302
|
});
|
|
303
303
|
|
|
304
|
-
setPaymentData({ clientSecret: paymentIntent.clientSecret, provider: paymentIntent.provider, checkoutId });
|
|
304
|
+
setPaymentData({ clientSecret: paymentIntent.clientSecret, provider: paymentIntent.provider, checkoutId, clientSdk: paymentIntent.clientSdk });
|
|
305
305
|
|
|
306
306
|
// Step 5: Initialize the correct payment provider
|
|
307
307
|
if (paymentIntent.provider === 'stripe' && stripeProvider) {
|
|
@@ -911,6 +911,10 @@ function ProductPage({ product, storeInfo }: { product: Product; storeInfo: Stor
|
|
|
911
911
|
? getVariantPrice(selectedVariant, product.basePrice).toString()
|
|
912
912
|
: product.salePrice || product.basePrice;
|
|
913
913
|
|
|
914
|
+
// \u2705 For catalog cards: use pre-computed priceMin/priceMax instead of iterating variants
|
|
915
|
+
// product.priceMin / product.priceMax are always set for VARIABLE products with explicit variant prices.
|
|
916
|
+
// product.priceVaries is true when the range should be shown as "\u20AA49 \u2013 \u20AA199".
|
|
917
|
+
|
|
914
918
|
// Build attribute buttons (size, color, etc.)
|
|
915
919
|
const allOptions = product.variants?.map(v => getVariantOptions(v)) || [];
|
|
916
920
|
const attrNames = [...new Set(allOptions.flatMap(opts => opts.map(o => o.name)))];
|
|
@@ -1125,6 +1129,7 @@ const total = cart.total;
|
|
|
1125
1129
|
|
|
1126
1130
|
### Coupons
|
|
1127
1131
|
|
|
1132
|
+
**On the cart page** (before checkout is created):
|
|
1128
1133
|
\`\`\`typescript
|
|
1129
1134
|
// Apply coupon \u2014 returns updated Cart with discountAmount
|
|
1130
1135
|
const updatedCart = await client.applyCoupon(cartId, 'SAVE20');
|
|
@@ -1132,13 +1137,24 @@ console.log(updatedCart.discountAmount); // "10.00"
|
|
|
1132
1137
|
console.log(updatedCart.couponCode); // "SAVE20"
|
|
1133
1138
|
|
|
1134
1139
|
// Remove coupon
|
|
1135
|
-
|
|
1140
|
+
await client.removeCoupon(cartId);
|
|
1136
1141
|
|
|
1137
1142
|
// Calculate totals including discount
|
|
1138
1143
|
const totals = getCartTotals(cart); // { subtotal, discount, shipping, total }
|
|
1139
1144
|
\`\`\`
|
|
1140
1145
|
|
|
1141
|
-
|
|
1146
|
+
**On the checkout page** (after checkout session exists \u2014 ALWAYS use this when checkoutId is available):
|
|
1147
|
+
\`\`\`typescript
|
|
1148
|
+
// Applies to cart AND updates checkout totals in one call
|
|
1149
|
+
const checkout = await client.applyCheckoutCoupon(checkoutId, 'SAVE20');
|
|
1150
|
+
console.log(checkout.discountAmount); // "10.00"
|
|
1151
|
+
console.log(checkout.total); // correctly updated total
|
|
1152
|
+
|
|
1153
|
+
// Remove coupon from checkout
|
|
1154
|
+
await client.removeCheckoutCoupon(checkoutId);
|
|
1155
|
+
\`\`\`
|
|
1156
|
+
|
|
1157
|
+
> \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.
|
|
1142
1158
|
|
|
1143
1159
|
### \u26A0\uFE0F Checkout Order Summary \u2014 Use checkout.lineItems, NOT cart.items!
|
|
1144
1160
|
|
|
@@ -1563,6 +1579,53 @@ The merchant can hide a group entirely for one variant by setting \`maxOverride:
|
|
|
1563
1579
|
|
|
1564
1580
|
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.`;
|
|
1565
1581
|
}
|
|
1582
|
+
function getProductReviewsSection() {
|
|
1583
|
+
return `## Product Reviews (\u2605 ratings + JSON-LD for SEO)
|
|
1584
|
+
|
|
1585
|
+
Reviews publish immediately on submit. Merchants hide individual reviews via the admin surface \u2014 no PENDING queue.
|
|
1586
|
+
|
|
1587
|
+
\`\`\`typescript
|
|
1588
|
+
// PDP \u2014 list visible reviews
|
|
1589
|
+
const { data, meta } = await client.listProductReviews(productId, { page: 1, limit: 20 });
|
|
1590
|
+
data.forEach(r => render(r.authorName, r.rating, r.body, r.verifiedPurchase));
|
|
1591
|
+
|
|
1592
|
+
// PDP \u2014 submit form
|
|
1593
|
+
await client.submitProductReview(productId, {
|
|
1594
|
+
authorName, authorEmail, rating: 5, body,
|
|
1595
|
+
});
|
|
1596
|
+
\`\`\`
|
|
1597
|
+
|
|
1598
|
+
Each \`Product\` carries denormalized rollups: \`product.avgRating\`, \`product.reviewCount\`. Use these on PLP cards.
|
|
1599
|
+
|
|
1600
|
+
### JSON-LD (mandatory for SEO stars in Google)
|
|
1601
|
+
|
|
1602
|
+
Emit \`aggregateRating\` ONLY when \`product.reviewCount > 0\`:
|
|
1603
|
+
|
|
1604
|
+
\`\`\`typescript
|
|
1605
|
+
const productJsonLd = {
|
|
1606
|
+
'@context': 'https://schema.org',
|
|
1607
|
+
'@type': 'Product',
|
|
1608
|
+
name: product.name,
|
|
1609
|
+
// \u2026existing fields
|
|
1610
|
+
...(product.reviewCount > 0 && {
|
|
1611
|
+
aggregateRating: {
|
|
1612
|
+
'@type': 'AggregateRating',
|
|
1613
|
+
ratingValue: product.avgRating,
|
|
1614
|
+
reviewCount: product.reviewCount,
|
|
1615
|
+
bestRating: 5,
|
|
1616
|
+
worstRating: 1,
|
|
1617
|
+
},
|
|
1618
|
+
}),
|
|
1619
|
+
};
|
|
1620
|
+
\`\`\`
|
|
1621
|
+
|
|
1622
|
+
### Rules
|
|
1623
|
+
- Rating must be an integer 1-5 (DB constraint).
|
|
1624
|
+
- Duplicate from same email \u2192 409 Conflict.
|
|
1625
|
+
- Submit endpoint is throttled to 3 / 60s / IP \u2014 surface a friendly "please wait" on 429.
|
|
1626
|
+
- \`verifiedPurchase\` is derived server-side from DELIVERED orders \u2014 DO NOT try to set it.
|
|
1627
|
+
- Stores with \`reviewsEnabled = false\` return 403 on storefront endpoints.`;
|
|
1628
|
+
}
|
|
1566
1629
|
function getInventorySection() {
|
|
1567
1630
|
return `## Inventory, Stock Display & Reservation Countdown
|
|
1568
1631
|
|
|
@@ -1958,7 +2021,7 @@ subsequent SDK call sends the \`Accept-Language\` header automatically:
|
|
|
1958
2021
|
\`\`\`typescript
|
|
1959
2022
|
import { BrainerceClient } from 'brainerce';
|
|
1960
2023
|
|
|
1961
|
-
const client = new BrainerceClient({
|
|
2024
|
+
const client = new BrainerceClient({ salesChannelId: 'vc_...' });
|
|
1962
2025
|
client.setLocale('he'); // done \u2014 all calls are now in Hebrew
|
|
1963
2026
|
\`\`\`
|
|
1964
2027
|
|
|
@@ -2509,6 +2572,8 @@ function getSectionByTopic(topic, connectionId, currency) {
|
|
|
2509
2572
|
return getDiscountsSection();
|
|
2510
2573
|
case "recommendations":
|
|
2511
2574
|
return getRecommendationsSection();
|
|
2575
|
+
case "reviews":
|
|
2576
|
+
return getProductReviewsSection();
|
|
2512
2577
|
case "product-customization-fields":
|
|
2513
2578
|
return getProductCustomizationFieldsSection();
|
|
2514
2579
|
case "modifier-groups":
|
|
@@ -2581,6 +2646,10 @@ function getSectionByTopic(topic, connectionId, currency) {
|
|
|
2581
2646
|
"",
|
|
2582
2647
|
"---",
|
|
2583
2648
|
"",
|
|
2649
|
+
getProductReviewsSection(),
|
|
2650
|
+
"",
|
|
2651
|
+
"---",
|
|
2652
|
+
"",
|
|
2584
2653
|
getDiscountsSection(),
|
|
2585
2654
|
"",
|
|
2586
2655
|
"---",
|
|
@@ -2632,6 +2701,7 @@ var GET_SDK_DOCS_SCHEMA = {
|
|
|
2632
2701
|
"inventory",
|
|
2633
2702
|
"discounts",
|
|
2634
2703
|
"recommendations",
|
|
2704
|
+
"reviews",
|
|
2635
2705
|
"product-customization-fields",
|
|
2636
2706
|
"tax",
|
|
2637
2707
|
"critical-rules",
|
|
@@ -2675,6 +2745,9 @@ interface Product {
|
|
|
2675
2745
|
basePrice: string; // Use parseFloat() for calculations
|
|
2676
2746
|
salePrice?: string | null;
|
|
2677
2747
|
costPrice?: string | null;
|
|
2748
|
+
priceMin?: string | null; // Lowest variant price (VARIABLE products). Use for catalog/JSON-LD range display.
|
|
2749
|
+
priceMax?: string | null; // Highest variant price (VARIABLE products).
|
|
2750
|
+
priceVaries?: boolean; // true when variant prices differ \u2014 show "\u20AA49 \u2013 \u20AA199" range.
|
|
2678
2751
|
status: string;
|
|
2679
2752
|
type: 'SIMPLE' | 'VARIABLE';
|
|
2680
2753
|
isDownloadable?: boolean;
|
|
@@ -3245,9 +3318,9 @@ var HELPERS_TYPES = `// ---- Helper Functions (import from 'brainerce') ----
|
|
|
3245
3318
|
// Price helpers
|
|
3246
3319
|
function formatPrice(priceString: string | number | undefined | null, options?: { currency?: string; locale?: string; }): string;
|
|
3247
3320
|
function getProductPrice(product: Pick<Product, 'basePrice' | 'salePrice'>): number;
|
|
3248
|
-
function getProductPriceInfo(product: Pick<Product, 'basePrice' | 'salePrice'>): {
|
|
3321
|
+
function getProductPriceInfo(product: Pick<Product, 'basePrice' | 'salePrice' | 'discount' | 'priceMin' | 'priceVaries'>): {
|
|
3249
3322
|
price: number; originalPrice: number; isOnSale: boolean; discountAmount: number; discountPercent: number;
|
|
3250
|
-
};
|
|
3323
|
+
}; // Falls back to priceMin when basePrice=0 (VARIABLE products)
|
|
3251
3324
|
function getVariantPrice(variant: Pick<ProductVariant, 'price' | 'salePrice'>, productBasePrice: string): number;
|
|
3252
3325
|
|
|
3253
3326
|
// Cart helpers
|
|
@@ -3360,6 +3433,37 @@ interface CartBundleOffer {
|
|
|
3360
3433
|
totalOriginalPrice: string;
|
|
3361
3434
|
totalDiscountedPrice: string;
|
|
3362
3435
|
}`;
|
|
3436
|
+
var REVIEWS_TYPES = `// ---- Product Reviews ----
|
|
3437
|
+
|
|
3438
|
+
interface ProductReview {
|
|
3439
|
+
id: string;
|
|
3440
|
+
productId: string;
|
|
3441
|
+
authorName: string; // max 100 chars
|
|
3442
|
+
rating: number; // integer 1-5
|
|
3443
|
+
body: string | null; // plain text, max 5000 chars
|
|
3444
|
+
verifiedPurchase: boolean; // derived server-side from DELIVERED orders
|
|
3445
|
+
hiddenAt?: string | null; // ISO datetime; admin responses only \u2014 null = visible
|
|
3446
|
+
createdAt: string; // ISO datetime
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
interface ProductReviewAdmin extends ProductReview {
|
|
3450
|
+
customerId: string | null;
|
|
3451
|
+
authorEmail: string | null;
|
|
3452
|
+
orderId: string | null;
|
|
3453
|
+
updatedAt: string;
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
interface SubmitProductReviewInput {
|
|
3457
|
+
authorName: string;
|
|
3458
|
+
authorEmail?: string; // recommended \u2014 used for guest dedupe + verified-purchase derivation
|
|
3459
|
+
rating: number; // 1-5
|
|
3460
|
+
body?: string; // optional
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
// Each Product carries denormalized review stats:
|
|
3464
|
+
// product.avgRating: number // 0 when reviewCount is 0
|
|
3465
|
+
// product.reviewCount: number // count of visible (hiddenAt IS NULL) reviews
|
|
3466
|
+
`;
|
|
3363
3467
|
var INQUIRIES_TYPES = `// ---- Contact Inquiries & Forms ----
|
|
3364
3468
|
|
|
3365
3469
|
// Two shapes \u2014 legacy and flexible. The two may be mixed; \`fields\` wins on key collision.
|
|
@@ -3603,6 +3707,7 @@ var TYPES_BY_DOMAIN = {
|
|
|
3603
3707
|
payments: PAYMENTS_TYPES,
|
|
3604
3708
|
helpers: HELPERS_TYPES,
|
|
3605
3709
|
inquiries: INQUIRIES_TYPES,
|
|
3710
|
+
reviews: REVIEWS_TYPES,
|
|
3606
3711
|
"modifier-groups": MODIFIER_GROUPS_TYPES
|
|
3607
3712
|
};
|
|
3608
3713
|
function getTypesByDomain(domain) {
|
|
@@ -3633,6 +3738,7 @@ var GET_TYPE_DEFINITIONS_SCHEMA = {
|
|
|
3633
3738
|
"payments",
|
|
3634
3739
|
"helpers",
|
|
3635
3740
|
"inquiries",
|
|
3741
|
+
"reviews",
|
|
3636
3742
|
"all"
|
|
3637
3743
|
]).describe(
|
|
3638
3744
|
'The domain of types to retrieve. Use "helpers" for helper function signatures and common types like StoreInfo, PaginatedResponse.'
|
|
@@ -3749,7 +3855,7 @@ function selectVariant(product: Product, selections: Record<string, string>) {
|
|
|
3749
3855
|
import { client } from './brainerce';
|
|
3750
3856
|
|
|
3751
3857
|
// 1. Submit the address + email. Email is REQUIRED on the DTO.
|
|
3752
|
-
const {
|
|
3858
|
+
const { checkout, rates } = await client.setShippingAddress(checkoutId, {
|
|
3753
3859
|
email,
|
|
3754
3860
|
firstName,
|
|
3755
3861
|
lastName,
|
|
@@ -3760,10 +3866,11 @@ const { shippingRates } = await client.setShippingAddress(checkoutId, {
|
|
|
3760
3866
|
postalCode,
|
|
3761
3867
|
country,
|
|
3762
3868
|
});
|
|
3869
|
+
// rates = available shipping rates; checkout = updated checkout object
|
|
3763
3870
|
|
|
3764
3871
|
// 2. Let the customer pick one of the returned rates.
|
|
3765
|
-
const chosen =
|
|
3766
|
-
await client.
|
|
3872
|
+
const chosen = rates[0]; // user selection
|
|
3873
|
+
await client.selectShippingMethod(checkoutId, chosen.id);
|
|
3767
3874
|
|
|
3768
3875
|
// 3. Re-fetch the checkout to get updated totals (tax may change after address).
|
|
3769
3876
|
const checkout = await client.getCheckout(checkoutId);
|
|
@@ -3817,21 +3924,21 @@ if (providers.length === 0) {
|
|
|
3817
3924
|
}
|
|
3818
3925
|
|
|
3819
3926
|
const active = providers[0];`,
|
|
3820
|
-
"checkout-stripe-confirm": `// Stripe confirm flow. Requires @stripe/stripe-js
|
|
3821
|
-
// (or the vanilla Stripe.js browser SDK).
|
|
3927
|
+
"checkout-stripe-confirm": `// Stripe confirm flow. Requires @stripe/stripe-js.
|
|
3822
3928
|
import { loadStripe } from '@stripe/stripe-js';
|
|
3823
3929
|
import { client } from './brainerce';
|
|
3824
3930
|
|
|
3825
3931
|
const stripe = await loadStripe(stripePublicKey);
|
|
3826
3932
|
if (!stripe) throw new Error('Stripe failed to load');
|
|
3827
3933
|
|
|
3828
|
-
//
|
|
3829
|
-
const
|
|
3830
|
-
|
|
3934
|
+
// Create a payment intent \u2014 returns clientSecret for Stripe Elements.
|
|
3935
|
+
const intent = await client.createPaymentIntent(checkoutId, {
|
|
3936
|
+
successUrl: \`\${window.location.origin}/order-confirmation?checkoutId=\${checkoutId}\`,
|
|
3937
|
+
cancelUrl: \`\${window.location.origin}/checkout?error=cancelled\`,
|
|
3831
3938
|
});
|
|
3832
3939
|
|
|
3833
|
-
// Use Stripe Elements to
|
|
3834
|
-
const result = await stripe.confirmCardPayment(clientSecret, {
|
|
3940
|
+
// Use Stripe Elements to confirm:
|
|
3941
|
+
const result = await stripe.confirmCardPayment(intent.clientSecret, {
|
|
3835
3942
|
payment_method: { card: cardElement },
|
|
3836
3943
|
});
|
|
3837
3944
|
|
|
@@ -3840,21 +3947,21 @@ if (result.error) {
|
|
|
3840
3947
|
return;
|
|
3841
3948
|
}
|
|
3842
3949
|
|
|
3843
|
-
// Success \u2014 redirect to
|
|
3950
|
+
// Success \u2014 redirect to confirmation page.
|
|
3844
3951
|
// The confirmation page runs handlePaymentSuccess + waitForOrder.
|
|
3845
3952
|
window.location.assign(\`/order-confirmation?checkoutId=\${checkoutId}\`);`,
|
|
3846
3953
|
"checkout-paypal-confirm": `// PayPal confirm flow. Use the PayPal JS SDK (render a PayPal button component).
|
|
3847
3954
|
import { client } from './brainerce';
|
|
3848
3955
|
|
|
3849
|
-
//
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
});
|
|
3956
|
+
// Create a payment intent first \u2014 for PayPal this returns the PayPal order ID.
|
|
3957
|
+
const intent = await client.createPaymentIntent(checkoutId, {
|
|
3958
|
+
successUrl: \`\${window.location.origin}/order-confirmation?checkoutId=\${checkoutId}\`,
|
|
3959
|
+
cancelUrl: \`\${window.location.origin}/checkout?error=cancelled\`,
|
|
3960
|
+
});
|
|
3855
3961
|
|
|
3856
|
-
|
|
3857
|
-
|
|
3962
|
+
// Render a PayPal button using intent.clientSdk (provider SDK config).
|
|
3963
|
+
// When the PayPal button's onApprove fires, redirect to confirmation:
|
|
3964
|
+
function onPayPalApprove() {
|
|
3858
3965
|
window.location.assign(\`/order-confirmation?checkoutId=\${checkoutId}\`);
|
|
3859
3966
|
}`,
|
|
3860
3967
|
"checkout-sandbox-confirm": `// Sandbox payment \u2014 store has sandboxPaymentsEnabled=true. No real charge.
|
|
@@ -3897,7 +4004,7 @@ async function registerAndMaybeVerify(input: {
|
|
|
3897
4004
|
firstName: string;
|
|
3898
4005
|
lastName: string;
|
|
3899
4006
|
}) {
|
|
3900
|
-
const result = await client.
|
|
4007
|
+
const result = await client.registerCustomer(input);
|
|
3901
4008
|
if (result.requiresVerification) {
|
|
3902
4009
|
// Route the user to your verify-email UI. Do NOT treat them as logged in.
|
|
3903
4010
|
return { state: 'needs-verification' as const };
|
|
@@ -3918,7 +4025,7 @@ async function resendCode() {
|
|
|
3918
4025
|
import { client } from './brainerce';
|
|
3919
4026
|
|
|
3920
4027
|
async function login(email: string, password: string) {
|
|
3921
|
-
const result = await client.
|
|
4028
|
+
const result = await client.loginCustomer(email, password);
|
|
3922
4029
|
if (result.requiresVerification) {
|
|
3923
4030
|
return { state: 'needs-verification' as const };
|
|
3924
4031
|
}
|
|
@@ -3953,25 +4060,37 @@ async function submitReset(token: string, newPassword: string) {
|
|
|
3953
4060
|
"oauth-redirect-and-callback": `// OAuth sign-in. Redirect flow, NOT popup.
|
|
3954
4061
|
import { client } from './brainerce';
|
|
3955
4062
|
|
|
3956
|
-
//
|
|
3957
|
-
const providers = await client.getAvailableOAuthProviders();
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
4063
|
+
// Step 1: Get available provider names (strings: 'GOOGLE', 'FACEBOOK', 'GITHUB')
|
|
4064
|
+
const { providers } = await client.getAvailableOAuthProviders();
|
|
4065
|
+
|
|
4066
|
+
// Step 2: For each provider, fetch the authorization URL, then redirect.
|
|
4067
|
+
// Call this when the user clicks a provider button:
|
|
4068
|
+
async function redirectToOAuth(provider: string) {
|
|
4069
|
+
const { authorizationUrl } = await client.getOAuthAuthorizeUrl(provider, {
|
|
4070
|
+
redirectUrl: \`\${window.location.origin}/auth/callback\`,
|
|
4071
|
+
});
|
|
4072
|
+
window.location.href = authorizationUrl; // full-page redirect, NOT a popup
|
|
3961
4073
|
}
|
|
3962
4074
|
|
|
3963
|
-
// On your
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
4075
|
+
// Step 3: On your /auth/callback route, read token from URL params:
|
|
4076
|
+
const params = new URLSearchParams(window.location.search);
|
|
4077
|
+
const oauthError = params.get('oauth_error');
|
|
4078
|
+
const token = params.get('token');
|
|
4079
|
+
if (oauthError) {
|
|
4080
|
+
window.location.href = '/login?error=' + encodeURIComponent(oauthError);
|
|
4081
|
+
} else if (token) {
|
|
4082
|
+
client.setCustomerToken(token);
|
|
4083
|
+
window.location.href = '/account';
|
|
4084
|
+
}`,
|
|
3969
4085
|
"coupon-apply-and-remove": `// Coupon input. Build the UI even if no coupons exist today \u2014 it auto-hides.
|
|
4086
|
+
// IMPORTANT: use applyCheckoutCoupon when a checkoutId is available (checkout page).
|
|
4087
|
+
// Using applyCoupon after checkout creation does NOT update the checkout total.
|
|
3970
4088
|
import { client } from './brainerce';
|
|
3971
4089
|
|
|
3972
|
-
|
|
4090
|
+
// On cart page (no checkout session yet)
|
|
4091
|
+
async function applyCouponToCart(cartId: string, code: string) {
|
|
3973
4092
|
try {
|
|
3974
|
-
const cart = await client.applyCoupon(code);
|
|
4093
|
+
const cart = await client.applyCoupon(cartId, code);
|
|
3975
4094
|
return { ok: true, cart };
|
|
3976
4095
|
} catch (err) {
|
|
3977
4096
|
// Invalid / expired / minimum not met. Surface the specific error.
|
|
@@ -3979,17 +4098,30 @@ async function applyCoupon(code: string) {
|
|
|
3979
4098
|
}
|
|
3980
4099
|
}
|
|
3981
4100
|
|
|
3982
|
-
async function
|
|
3983
|
-
|
|
3984
|
-
|
|
4101
|
+
async function removeCouponFromCart(cartId: string) {
|
|
4102
|
+
return client.removeCoupon(cartId);
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
// On checkout page (checkout session already exists \u2014 always prefer this)
|
|
4106
|
+
async function applyCouponToCheckout(checkoutId: string, code: string) {
|
|
4107
|
+
try {
|
|
4108
|
+
const checkout = await client.applyCheckoutCoupon(checkoutId, code);
|
|
4109
|
+
return { ok: true, checkout }; // checkout.total is already updated
|
|
4110
|
+
} catch (err) {
|
|
4111
|
+
return { ok: false, error: (err as Error).message };
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
|
|
4115
|
+
async function removeCouponFromCheckout(checkoutId: string) {
|
|
4116
|
+
return client.removeCheckoutCoupon(checkoutId);
|
|
3985
4117
|
}`,
|
|
3986
4118
|
"reservation-countdown": `// Reservation countdown. Use the SDK's expiry timestamp \u2014 do NOT invent
|
|
3987
4119
|
// your own timer state.
|
|
3988
4120
|
import { client } from './brainerce';
|
|
3989
4121
|
|
|
3990
|
-
function getRemainingMs(cart: {
|
|
3991
|
-
if (!cart.
|
|
3992
|
-
const expiry = new Date(cart.
|
|
4122
|
+
function getRemainingMs(cart: { reservation?: { expiresAt?: string } }): number {
|
|
4123
|
+
if (!cart.reservation?.expiresAt) return 0;
|
|
4124
|
+
const expiry = new Date(cart.reservation.expiresAt).getTime();
|
|
3993
4125
|
return Math.max(0, expiry - Date.now());
|
|
3994
4126
|
}
|
|
3995
4127
|
|
|
@@ -4016,8 +4148,8 @@ function onSearchChange(query: string, onResults: (items: unknown[]) => void) {
|
|
|
4016
4148
|
return;
|
|
4017
4149
|
}
|
|
4018
4150
|
searchTimer = setTimeout(async () => {
|
|
4019
|
-
const
|
|
4020
|
-
onResults(
|
|
4151
|
+
const { products } = await client.getSearchSuggestions(query);
|
|
4152
|
+
onResults(products);
|
|
4021
4153
|
}, 300);
|
|
4022
4154
|
}`,
|
|
4023
4155
|
"i18n-set-locale": `// i18n \u2014 only if get-store-capabilities reports i18n.enabled.
|
|
@@ -4374,19 +4506,13 @@ import { client } from './brainerce-client';
|
|
|
4374
4506
|
|
|
4375
4507
|
interface CheckoutOptions {
|
|
4376
4508
|
cartId: string;
|
|
4377
|
-
shippingAddress: object;
|
|
4378
|
-
shippingMethodId: string;
|
|
4379
|
-
paymentProviderId: string;
|
|
4380
4509
|
}
|
|
4381
4510
|
|
|
4382
4511
|
async function createCheckoutSafe(opts: CheckoutOptions) {
|
|
4383
4512
|
try {
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
shippingMethodId: opts.shippingMethodId,
|
|
4388
|
-
paymentProviderId: opts.paymentProviderId,
|
|
4389
|
-
});
|
|
4513
|
+
// createCheckout only takes cartId (+ optional customerId, selectedItemIds).
|
|
4514
|
+
// Shipping address, shipping method, and payment are set via separate calls.
|
|
4515
|
+
return await client.createCheckout({ cartId: opts.cartId });
|
|
4390
4516
|
} catch (err: unknown) {
|
|
4391
4517
|
if (!isApiError(err, 'PRICE_DRIFT')) throw err;
|
|
4392
4518
|
|
|
@@ -4516,6 +4642,93 @@ function renderCartItem(item: CartItem): string {
|
|
|
4516
4642
|
// multiply unitPrice \xD7 quantity directly.
|
|
4517
4643
|
function lineTotal(item: CartItem): number {
|
|
4518
4644
|
return item.unitPrice * item.quantity;
|
|
4645
|
+
}`,
|
|
4646
|
+
"product-reviews": `// PDP: list visible reviews + submit form + JSON-LD aggregateRating
|
|
4647
|
+
import { client } from '@/lib/brainerce';
|
|
4648
|
+
import type { Product, ProductReview, SubmitProductReviewInput } from 'brainerce';
|
|
4649
|
+
|
|
4650
|
+
export async function ProductReviewsSection({ product }: { product: Product }) {
|
|
4651
|
+
const { data } = await client.listProductReviews(product.id, { page: 1, limit: 20 });
|
|
4652
|
+
|
|
4653
|
+
return (
|
|
4654
|
+
<section>
|
|
4655
|
+
<h2>Reviews ({product.reviewCount ?? 0})</h2>
|
|
4656
|
+
{data.length === 0 && <p>No reviews yet \u2014 be the first.</p>}
|
|
4657
|
+
{data.map((r) => (
|
|
4658
|
+
<article key={r.id}>
|
|
4659
|
+
<strong>{r.authorName}</strong>
|
|
4660
|
+
{r.verifiedPurchase && <span> \xB7 Verified</span>}
|
|
4661
|
+
<div>{'\u2605'.repeat(r.rating)}{'\u2606'.repeat(5 - r.rating)}</div>
|
|
4662
|
+
{r.body && <p>{r.body}</p>}
|
|
4663
|
+
</article>
|
|
4664
|
+
))}
|
|
4665
|
+
<ReviewForm productId={product.id} />
|
|
4666
|
+
</section>
|
|
4667
|
+
);
|
|
4668
|
+
}
|
|
4669
|
+
|
|
4670
|
+
// JSON-LD \u2014 emit aggregateRating only when there is data
|
|
4671
|
+
function productJsonLd(product: Product, url: string) {
|
|
4672
|
+
return {
|
|
4673
|
+
'@context': 'https://schema.org',
|
|
4674
|
+
'@type': 'Product',
|
|
4675
|
+
name: product.name,
|
|
4676
|
+
image: product.images?.[0]?.url,
|
|
4677
|
+
url,
|
|
4678
|
+
sku: product.sku ?? product.id,
|
|
4679
|
+
...(product.reviewCount && product.reviewCount > 0 && {
|
|
4680
|
+
aggregateRating: {
|
|
4681
|
+
'@type': 'AggregateRating',
|
|
4682
|
+
ratingValue: product.avgRating,
|
|
4683
|
+
reviewCount: product.reviewCount,
|
|
4684
|
+
bestRating: 5,
|
|
4685
|
+
worstRating: 1,
|
|
4686
|
+
},
|
|
4687
|
+
}),
|
|
4688
|
+
};
|
|
4689
|
+
}
|
|
4690
|
+
|
|
4691
|
+
// Client-side form
|
|
4692
|
+
'use client';
|
|
4693
|
+
import { useState } from 'react';
|
|
4694
|
+
|
|
4695
|
+
function ReviewForm({ productId }: { productId: string }) {
|
|
4696
|
+
const [rating, setRating] = useState(5);
|
|
4697
|
+
const [body, setBody] = useState('');
|
|
4698
|
+
const [name, setName] = useState('');
|
|
4699
|
+
const [email, setEmail] = useState('');
|
|
4700
|
+
const [error, setError] = useState<string | null>(null);
|
|
4701
|
+
|
|
4702
|
+
async function submit(e: React.FormEvent) {
|
|
4703
|
+
e.preventDefault();
|
|
4704
|
+
setError(null);
|
|
4705
|
+
try {
|
|
4706
|
+
await client.submitProductReview(productId, {
|
|
4707
|
+
authorName: name,
|
|
4708
|
+
authorEmail: email,
|
|
4709
|
+
rating,
|
|
4710
|
+
body: body || undefined,
|
|
4711
|
+
});
|
|
4712
|
+
window.location.reload();
|
|
4713
|
+
} catch (err: any) {
|
|
4714
|
+
if (err?.status === 409) setError("You've already reviewed this product.");
|
|
4715
|
+
else if (err?.status === 429) setError('Too many submissions. Please wait a moment.');
|
|
4716
|
+
else setError('Could not submit review. Please try again.');
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
|
|
4720
|
+
return (
|
|
4721
|
+
<form onSubmit={submit}>
|
|
4722
|
+
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="Your name" required maxLength={100} />
|
|
4723
|
+
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
|
|
4724
|
+
<select value={rating} onChange={(e) => setRating(Number(e.target.value))}>
|
|
4725
|
+
{[5,4,3,2,1].map(n => <option key={n} value={n}>{'\u2605'.repeat(n)}</option>)}
|
|
4726
|
+
</select>
|
|
4727
|
+
<textarea value={body} onChange={(e) => setBody(e.target.value)} maxLength={5000} placeholder="Tell us what you think (optional)" />
|
|
4728
|
+
<button type="submit">Submit review</button>
|
|
4729
|
+
{error && <p role="alert">{error}</p>}
|
|
4730
|
+
</form>
|
|
4731
|
+
);
|
|
4519
4732
|
}`
|
|
4520
4733
|
};
|
|
4521
4734
|
async function handleGetCodeExample(args) {
|
|
@@ -5061,21 +5274,21 @@ var FLOWS = {
|
|
|
5061
5274
|
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\`.
|
|
5062
5275
|
2. **Submit the address to get shipping rates.**
|
|
5063
5276
|
\`\`\`ts
|
|
5064
|
-
const {
|
|
5277
|
+
const { checkout, rates } = await client.setShippingAddress(checkoutId, {
|
|
5065
5278
|
email,
|
|
5066
5279
|
firstName,
|
|
5067
5280
|
lastName,
|
|
5068
5281
|
line1, line2, city, region, postalCode, country,
|
|
5069
5282
|
});
|
|
5070
5283
|
\`\`\`
|
|
5071
|
-
|
|
5284
|
+
\`rates\` = available shipping rates for that address + the store's zones. \`checkout\` = updated checkout object.
|
|
5072
5285
|
3. **Let the customer pick a rate**, then persist the selection:
|
|
5073
5286
|
\`\`\`ts
|
|
5074
|
-
await client.
|
|
5287
|
+
await client.selectShippingMethod(checkoutId, rateId);
|
|
5075
5288
|
\`\`\`
|
|
5076
|
-
4. **Fetch payment providers
|
|
5289
|
+
4. **Fetch payment providers:**
|
|
5077
5290
|
\`\`\`ts
|
|
5078
|
-
const providers = await client.getPaymentProviders(
|
|
5291
|
+
const providers = await client.getPaymentProviders();
|
|
5079
5292
|
\`\`\`
|
|
5080
5293
|
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.
|
|
5081
5294
|
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.
|
|
@@ -5091,28 +5304,28 @@ Never bypass these steps. Never call \`submitGuestOrder\` / \`createOrder\` \u20
|
|
|
5091
5304
|
"auth-register": {
|
|
5092
5305
|
title: "Registration",
|
|
5093
5306
|
body: `1. **Collect** email, password, first name, last name. Enforce strong passwords client-side: 8+ chars, upper, lower, number, special.
|
|
5094
|
-
2. **Call
|
|
5307
|
+
2. **Call registerCustomer:**
|
|
5095
5308
|
\`\`\`ts
|
|
5096
|
-
const result = await client.
|
|
5309
|
+
const result = await client.registerCustomer({ email, password, firstName, lastName });
|
|
5097
5310
|
\`\`\`
|
|
5098
5311
|
3. **Branch on \`result.requiresVerification\`:**
|
|
5099
|
-
- If \`true\`: route the user to your verify-email UI. Do NOT treat them as logged in yet.
|
|
5100
|
-
- If \`false\`:
|
|
5312
|
+
- 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.
|
|
5313
|
+
- If \`false\`: call \`client.setCustomerToken(result.token)\` and route to the account area.
|
|
5101
5314
|
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()\`.
|
|
5102
|
-
5. **After verifyEmail resolves:** the user is now logged in
|
|
5315
|
+
5. **After verifyEmail resolves:** call \`client.setCustomerToken(result.token)\` \u2014 the user is now logged in \u2014 then route to the account area.
|
|
5103
5316
|
|
|
5104
5317
|
Build the verify-email step EVEN IF the store currently has verification disabled. It auto-hides; store owners enable it later.`
|
|
5105
5318
|
},
|
|
5106
5319
|
"auth-login": {
|
|
5107
5320
|
title: "Login",
|
|
5108
5321
|
body: `1. **Collect** email + password.
|
|
5109
|
-
2. **Call
|
|
5322
|
+
2. **Call loginCustomer:**
|
|
5110
5323
|
\`\`\`ts
|
|
5111
|
-
const result = await client.
|
|
5324
|
+
const result = await client.loginCustomer(email, password);
|
|
5112
5325
|
\`\`\`
|
|
5113
5326
|
3. **Branch on \`result.requiresVerification\`:**
|
|
5114
5327
|
- If \`true\`: route to verify-email. The user must complete verification before accessing account features.
|
|
5115
|
-
- If \`false\`:
|
|
5328
|
+
- If \`false\`: call \`client.setCustomerToken(result.token)\` and route to the previous page (or account area).
|
|
5116
5329
|
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.
|
|
5117
5330
|
5. **On error**, render the specific message (invalid credentials, rate limited, account disabled) \u2014 never swallow.`
|
|
5118
5331
|
},
|
|
@@ -5137,15 +5350,24 @@ Build both steps EVEN IF the store has no email provider configured today \u2014
|
|
|
5137
5350
|
},
|
|
5138
5351
|
oauth: {
|
|
5139
5352
|
title: "OAuth sign-in",
|
|
5140
|
-
body: `1. **
|
|
5353
|
+
body: `1. **Get available provider names:**
|
|
5141
5354
|
\`\`\`ts
|
|
5142
|
-
const providers = await client.getAvailableOAuthProviders();
|
|
5355
|
+
const { providers } = await client.getAvailableOAuthProviders();
|
|
5356
|
+
// providers = ['GOOGLE', 'FACEBOOK', 'GITHUB'] (strings, not objects with authorizationUrl)
|
|
5357
|
+
\`\`\`
|
|
5358
|
+
2. **For each provider, fetch the authorization URL:**
|
|
5359
|
+
\`\`\`ts
|
|
5360
|
+
const { authorizationUrl } = await client.getOAuthAuthorizeUrl(provider, {
|
|
5361
|
+
redirectUrl: \`\${window.location.origin}/auth/callback\`,
|
|
5362
|
+
});
|
|
5363
|
+
window.location.href = authorizationUrl; // full-page redirect, NOT a popup
|
|
5143
5364
|
\`\`\`
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5365
|
+
3. **On the callback page** the URL contains \`token\` + \`oauth_success\` (or \`oauth_error\`) query params. Extract and apply the token:
|
|
5366
|
+
\`\`\`ts
|
|
5367
|
+
const token = new URLSearchParams(location.search).get('token');
|
|
5368
|
+
if (token) client.setCustomerToken(token); // then redirect to account
|
|
5369
|
+
\`\`\`
|
|
5370
|
+
4. **On \`oauth_error\`:** redirect to login with an error message.
|
|
5149
5371
|
|
|
5150
5372
|
Build the OAuth button region AND the callback handler even when no providers are configured. They auto-hide.`
|
|
5151
5373
|
},
|
|
@@ -5167,7 +5389,7 @@ Build the OAuth button region AND the callback handler even when no providers ar
|
|
|
5167
5389
|
|
|
5168
5390
|
- **Cart ID persistence:** the SDK stores the cart ID across reloads. You do not need to write cart-to-localStorage code yourself.
|
|
5169
5391
|
- **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).
|
|
5170
|
-
- **Writes:** use \`client.addToCart\`, \`client.updateCartItem\`, \`client.removeCartItem\`, \`client.applyCoupon\`, \`client.removeCoupon\`. After each mutation the SDK returns the updated cart.
|
|
5392
|
+
- **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.
|
|
5171
5393
|
- **Totals:** call \`getCartTotals(cart)\` \u2014 do NOT read \`cart.total\`. The helper understands taxes, shipping, and discounts.
|
|
5172
5394
|
- **\`smartGetCart()\`** returns \`CartWithIncludes\` (extends \`Cart\`). All carts are server-side. Pass \`{ include: ['recommendations', 'upgrades', 'bundles'] }\` to fetch extras in one request.
|
|
5173
5395
|
- **NEVER mutate cart state outside SDK helpers.** Any hand-rolled cart update risks desync with the reservation timer and the checkout flow.`
|
|
@@ -5299,7 +5521,7 @@ var FEATURES = [
|
|
|
5299
5521
|
id: "browse-products",
|
|
5300
5522
|
title: "Browse, filter, and search products",
|
|
5301
5523
|
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.",
|
|
5302
|
-
sdk: "client.getProducts({ page, limit, filters, sort, metafields }), client.getPublicMetafieldDefinitions(), client.
|
|
5524
|
+
sdk: "client.getProducts({ page, limit, filters, sort, metafields }), client.getPublicMetafieldDefinitions(), client.getSearchSuggestions(query)",
|
|
5303
5525
|
mandatory: "mandatory"
|
|
5304
5526
|
},
|
|
5305
5527
|
{
|
|
@@ -5309,6 +5531,13 @@ var FEATURES = [
|
|
|
5309
5531
|
sdk: "client.getProductBySlug(slug). Helpers: getProductPriceInfo, getVariantPrice, getStockStatus, getVariantOptions, getProductSwatches, getDescriptionContent, getProductMetafieldValue.",
|
|
5310
5532
|
mandatory: "mandatory"
|
|
5311
5533
|
},
|
|
5534
|
+
{
|
|
5535
|
+
id: "product-reviews",
|
|
5536
|
+
title: "Display + collect product reviews with stars + JSON-LD",
|
|
5537
|
+
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.',
|
|
5538
|
+
sdk: "client.listProductReviews(productId), client.submitProductReview(productId, { authorName, authorEmail, rating, body }). Product.avgRating + Product.reviewCount are denormalized rollups returned on every product fetch.",
|
|
5539
|
+
mandatory: "mandatory"
|
|
5540
|
+
},
|
|
5312
5541
|
{
|
|
5313
5542
|
id: "product-customization-fields",
|
|
5314
5543
|
title: "Render buyer-input customization fields on the product page",
|
|
@@ -5329,7 +5558,7 @@ var FEATURES = [
|
|
|
5329
5558
|
id: "cart-coupons",
|
|
5330
5559
|
title: "Apply and remove coupon codes",
|
|
5331
5560
|
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.",
|
|
5332
|
-
sdk: "client.applyCoupon(code), client.removeCoupon()",
|
|
5561
|
+
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)",
|
|
5333
5562
|
mandatory: "mandatory",
|
|
5334
5563
|
capabilityFlag: "hasCoupons",
|
|
5335
5564
|
whenDisabledNote: "Store has no coupons configured today. Build the UI anyway \u2014 it auto-hides until the store owner creates a coupon."
|
|
@@ -5346,7 +5575,7 @@ var FEATURES = [
|
|
|
5346
5575
|
id: "checkout",
|
|
5347
5576
|
title: "Complete a full checkout end-to-end",
|
|
5348
5577
|
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.",
|
|
5349
|
-
sdk: "client.setShippingAddress, client.
|
|
5578
|
+
sdk: "client.setShippingAddress (returns { checkout, rates }), client.selectShippingMethod, client.getPaymentProviders(), provider-specific confirm, client.handlePaymentSuccess, client.waitForOrder",
|
|
5350
5579
|
flowRef: "checkout",
|
|
5351
5580
|
mandatory: "mandatory"
|
|
5352
5581
|
},
|
|
@@ -5362,7 +5591,7 @@ var FEATURES = [
|
|
|
5362
5591
|
id: "register",
|
|
5363
5592
|
title: "Register a new account with email verification",
|
|
5364
5593
|
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.',
|
|
5365
|
-
sdk: "client.
|
|
5594
|
+
sdk: "client.registerCustomer({email, password, firstName, lastName}), client.verifyEmail(code), client.resendVerificationEmail()",
|
|
5366
5595
|
flowRef: "auth-register",
|
|
5367
5596
|
mandatory: "mandatory",
|
|
5368
5597
|
capabilityFlag: "oauth",
|
|
@@ -5372,7 +5601,7 @@ var FEATURES = [
|
|
|
5372
5601
|
id: "login",
|
|
5373
5602
|
title: "Log in with email / password and handle verification branch",
|
|
5374
5603
|
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).",
|
|
5375
|
-
sdk: "client.
|
|
5604
|
+
sdk: "client.loginCustomer(email, password)",
|
|
5376
5605
|
flowRef: "auth-login",
|
|
5377
5606
|
mandatory: "mandatory"
|
|
5378
5607
|
},
|
|
@@ -5405,14 +5634,14 @@ var FEATURES = [
|
|
|
5405
5634
|
id: "header",
|
|
5406
5635
|
title: "Global header with cart count and search",
|
|
5407
5636
|
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.",
|
|
5408
|
-
sdk: "client.getCart() for the count, client.
|
|
5637
|
+
sdk: "client.getCart() for the count, client.getSearchSuggestions(query) for autocomplete",
|
|
5409
5638
|
mandatory: "mandatory"
|
|
5410
5639
|
},
|
|
5411
5640
|
{
|
|
5412
5641
|
id: "discount-banners",
|
|
5413
5642
|
title: "Show active discount banners and badges",
|
|
5414
5643
|
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.",
|
|
5415
|
-
sdk: "client.getDiscountBanners(), getProductDiscountBadge(
|
|
5644
|
+
sdk: "client.getDiscountBanners(), client.getProductDiscountBadge(productId)",
|
|
5416
5645
|
mandatory: "mandatory",
|
|
5417
5646
|
capabilityFlag: "hasDiscountRules",
|
|
5418
5647
|
whenDisabledNote: "Store has no discount rules active. Build the banner and badge components anyway \u2014 they auto-hide."
|