@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.mjs
CHANGED
|
@@ -87,7 +87,7 @@ npm install brainerce
|
|
|
87
87
|
import { BrainerceClient } from 'brainerce';
|
|
88
88
|
|
|
89
89
|
export const client = new BrainerceClient({
|
|
90
|
-
|
|
90
|
+
salesChannelId: '${connectionId}',
|
|
91
91
|
});
|
|
92
92
|
|
|
93
93
|
// Cart helpers \u2014 save cart ID to localStorage
|
|
@@ -189,7 +189,7 @@ async function startCheckout() {
|
|
|
189
189
|
### Full Checkout Flow (Multi-Provider)
|
|
190
190
|
|
|
191
191
|
1. Customer fills cart
|
|
192
|
-
2. Customer optionally applies coupon
|
|
192
|
+
2. Customer optionally applies coupon on cart page \u2192 \`applyCoupon(cartId, code)\`
|
|
193
193
|
3. Detect payment providers \u2192 \`getPaymentProviders()\`
|
|
194
194
|
4. Start checkout session (\`startGuestCheckout()\` or \`createCheckout()\`)
|
|
195
195
|
5. Set shipping address (includes required email) \u2192 \`setShippingAddress()\`
|
|
@@ -207,7 +207,7 @@ import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-
|
|
|
207
207
|
|
|
208
208
|
function CheckoutPage() {
|
|
209
209
|
const [paymentData, setPaymentData] = useState<{
|
|
210
|
-
clientSecret: string; provider: string; checkoutId: string;
|
|
210
|
+
clientSecret: string; provider: string; checkoutId: string; clientSdk?: { renderType: string };
|
|
211
211
|
} | null>(null);
|
|
212
212
|
const [stripePromise, setStripePromise] = useState<ReturnType<typeof loadStripe> | null>(null);
|
|
213
213
|
const [paypalClientId, setPaypalClientId] = useState<string | null>(null);
|
|
@@ -269,7 +269,7 @@ function CheckoutPage() {
|
|
|
269
269
|
// saveCard: customerOptedIn, // optional opt-in
|
|
270
270
|
});
|
|
271
271
|
|
|
272
|
-
setPaymentData({ clientSecret: paymentIntent.clientSecret, provider: paymentIntent.provider, checkoutId });
|
|
272
|
+
setPaymentData({ clientSecret: paymentIntent.clientSecret, provider: paymentIntent.provider, checkoutId, clientSdk: paymentIntent.clientSdk });
|
|
273
273
|
|
|
274
274
|
// Step 5: Initialize the correct payment provider
|
|
275
275
|
if (paymentIntent.provider === 'stripe' && stripeProvider) {
|
|
@@ -879,6 +879,10 @@ function ProductPage({ product, storeInfo }: { product: Product; storeInfo: Stor
|
|
|
879
879
|
? getVariantPrice(selectedVariant, product.basePrice).toString()
|
|
880
880
|
: product.salePrice || product.basePrice;
|
|
881
881
|
|
|
882
|
+
// \u2705 For catalog cards: use pre-computed priceMin/priceMax instead of iterating variants
|
|
883
|
+
// product.priceMin / product.priceMax are always set for VARIABLE products with explicit variant prices.
|
|
884
|
+
// product.priceVaries is true when the range should be shown as "\u20AA49 \u2013 \u20AA199".
|
|
885
|
+
|
|
882
886
|
// Build attribute buttons (size, color, etc.)
|
|
883
887
|
const allOptions = product.variants?.map(v => getVariantOptions(v)) || [];
|
|
884
888
|
const attrNames = [...new Set(allOptions.flatMap(opts => opts.map(o => o.name)))];
|
|
@@ -1093,6 +1097,7 @@ const total = cart.total;
|
|
|
1093
1097
|
|
|
1094
1098
|
### Coupons
|
|
1095
1099
|
|
|
1100
|
+
**On the cart page** (before checkout is created):
|
|
1096
1101
|
\`\`\`typescript
|
|
1097
1102
|
// Apply coupon \u2014 returns updated Cart with discountAmount
|
|
1098
1103
|
const updatedCart = await client.applyCoupon(cartId, 'SAVE20');
|
|
@@ -1100,13 +1105,24 @@ console.log(updatedCart.discountAmount); // "10.00"
|
|
|
1100
1105
|
console.log(updatedCart.couponCode); // "SAVE20"
|
|
1101
1106
|
|
|
1102
1107
|
// Remove coupon
|
|
1103
|
-
|
|
1108
|
+
await client.removeCoupon(cartId);
|
|
1104
1109
|
|
|
1105
1110
|
// Calculate totals including discount
|
|
1106
1111
|
const totals = getCartTotals(cart); // { subtotal, discount, shipping, total }
|
|
1107
1112
|
\`\`\`
|
|
1108
1113
|
|
|
1109
|
-
|
|
1114
|
+
**On the checkout page** (after checkout session exists \u2014 ALWAYS use this when checkoutId is available):
|
|
1115
|
+
\`\`\`typescript
|
|
1116
|
+
// Applies to cart AND updates checkout totals in one call
|
|
1117
|
+
const checkout = await client.applyCheckoutCoupon(checkoutId, 'SAVE20');
|
|
1118
|
+
console.log(checkout.discountAmount); // "10.00"
|
|
1119
|
+
console.log(checkout.total); // correctly updated total
|
|
1120
|
+
|
|
1121
|
+
// Remove coupon from checkout
|
|
1122
|
+
await client.removeCheckoutCoupon(checkoutId);
|
|
1123
|
+
\`\`\`
|
|
1124
|
+
|
|
1125
|
+
> \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.
|
|
1110
1126
|
|
|
1111
1127
|
### \u26A0\uFE0F Checkout Order Summary \u2014 Use checkout.lineItems, NOT cart.items!
|
|
1112
1128
|
|
|
@@ -1531,6 +1547,53 @@ The merchant can hide a group entirely for one variant by setting \`maxOverride:
|
|
|
1531
1547
|
|
|
1532
1548
|
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.`;
|
|
1533
1549
|
}
|
|
1550
|
+
function getProductReviewsSection() {
|
|
1551
|
+
return `## Product Reviews (\u2605 ratings + JSON-LD for SEO)
|
|
1552
|
+
|
|
1553
|
+
Reviews publish immediately on submit. Merchants hide individual reviews via the admin surface \u2014 no PENDING queue.
|
|
1554
|
+
|
|
1555
|
+
\`\`\`typescript
|
|
1556
|
+
// PDP \u2014 list visible reviews
|
|
1557
|
+
const { data, meta } = await client.listProductReviews(productId, { page: 1, limit: 20 });
|
|
1558
|
+
data.forEach(r => render(r.authorName, r.rating, r.body, r.verifiedPurchase));
|
|
1559
|
+
|
|
1560
|
+
// PDP \u2014 submit form
|
|
1561
|
+
await client.submitProductReview(productId, {
|
|
1562
|
+
authorName, authorEmail, rating: 5, body,
|
|
1563
|
+
});
|
|
1564
|
+
\`\`\`
|
|
1565
|
+
|
|
1566
|
+
Each \`Product\` carries denormalized rollups: \`product.avgRating\`, \`product.reviewCount\`. Use these on PLP cards.
|
|
1567
|
+
|
|
1568
|
+
### JSON-LD (mandatory for SEO stars in Google)
|
|
1569
|
+
|
|
1570
|
+
Emit \`aggregateRating\` ONLY when \`product.reviewCount > 0\`:
|
|
1571
|
+
|
|
1572
|
+
\`\`\`typescript
|
|
1573
|
+
const productJsonLd = {
|
|
1574
|
+
'@context': 'https://schema.org',
|
|
1575
|
+
'@type': 'Product',
|
|
1576
|
+
name: product.name,
|
|
1577
|
+
// \u2026existing fields
|
|
1578
|
+
...(product.reviewCount > 0 && {
|
|
1579
|
+
aggregateRating: {
|
|
1580
|
+
'@type': 'AggregateRating',
|
|
1581
|
+
ratingValue: product.avgRating,
|
|
1582
|
+
reviewCount: product.reviewCount,
|
|
1583
|
+
bestRating: 5,
|
|
1584
|
+
worstRating: 1,
|
|
1585
|
+
},
|
|
1586
|
+
}),
|
|
1587
|
+
};
|
|
1588
|
+
\`\`\`
|
|
1589
|
+
|
|
1590
|
+
### Rules
|
|
1591
|
+
- Rating must be an integer 1-5 (DB constraint).
|
|
1592
|
+
- Duplicate from same email \u2192 409 Conflict.
|
|
1593
|
+
- Submit endpoint is throttled to 3 / 60s / IP \u2014 surface a friendly "please wait" on 429.
|
|
1594
|
+
- \`verifiedPurchase\` is derived server-side from DELIVERED orders \u2014 DO NOT try to set it.
|
|
1595
|
+
- Stores with \`reviewsEnabled = false\` return 403 on storefront endpoints.`;
|
|
1596
|
+
}
|
|
1534
1597
|
function getInventorySection() {
|
|
1535
1598
|
return `## Inventory, Stock Display & Reservation Countdown
|
|
1536
1599
|
|
|
@@ -1926,7 +1989,7 @@ subsequent SDK call sends the \`Accept-Language\` header automatically:
|
|
|
1926
1989
|
\`\`\`typescript
|
|
1927
1990
|
import { BrainerceClient } from 'brainerce';
|
|
1928
1991
|
|
|
1929
|
-
const client = new BrainerceClient({
|
|
1992
|
+
const client = new BrainerceClient({ salesChannelId: 'vc_...' });
|
|
1930
1993
|
client.setLocale('he'); // done \u2014 all calls are now in Hebrew
|
|
1931
1994
|
\`\`\`
|
|
1932
1995
|
|
|
@@ -2477,6 +2540,8 @@ function getSectionByTopic(topic, connectionId, currency) {
|
|
|
2477
2540
|
return getDiscountsSection();
|
|
2478
2541
|
case "recommendations":
|
|
2479
2542
|
return getRecommendationsSection();
|
|
2543
|
+
case "reviews":
|
|
2544
|
+
return getProductReviewsSection();
|
|
2480
2545
|
case "product-customization-fields":
|
|
2481
2546
|
return getProductCustomizationFieldsSection();
|
|
2482
2547
|
case "modifier-groups":
|
|
@@ -2549,6 +2614,10 @@ function getSectionByTopic(topic, connectionId, currency) {
|
|
|
2549
2614
|
"",
|
|
2550
2615
|
"---",
|
|
2551
2616
|
"",
|
|
2617
|
+
getProductReviewsSection(),
|
|
2618
|
+
"",
|
|
2619
|
+
"---",
|
|
2620
|
+
"",
|
|
2552
2621
|
getDiscountsSection(),
|
|
2553
2622
|
"",
|
|
2554
2623
|
"---",
|
|
@@ -2600,6 +2669,7 @@ var GET_SDK_DOCS_SCHEMA = {
|
|
|
2600
2669
|
"inventory",
|
|
2601
2670
|
"discounts",
|
|
2602
2671
|
"recommendations",
|
|
2672
|
+
"reviews",
|
|
2603
2673
|
"product-customization-fields",
|
|
2604
2674
|
"tax",
|
|
2605
2675
|
"critical-rules",
|
|
@@ -2643,6 +2713,9 @@ interface Product {
|
|
|
2643
2713
|
basePrice: string; // Use parseFloat() for calculations
|
|
2644
2714
|
salePrice?: string | null;
|
|
2645
2715
|
costPrice?: string | null;
|
|
2716
|
+
priceMin?: string | null; // Lowest variant price (VARIABLE products). Use for catalog/JSON-LD range display.
|
|
2717
|
+
priceMax?: string | null; // Highest variant price (VARIABLE products).
|
|
2718
|
+
priceVaries?: boolean; // true when variant prices differ \u2014 show "\u20AA49 \u2013 \u20AA199" range.
|
|
2646
2719
|
status: string;
|
|
2647
2720
|
type: 'SIMPLE' | 'VARIABLE';
|
|
2648
2721
|
isDownloadable?: boolean;
|
|
@@ -3213,9 +3286,9 @@ var HELPERS_TYPES = `// ---- Helper Functions (import from 'brainerce') ----
|
|
|
3213
3286
|
// Price helpers
|
|
3214
3287
|
function formatPrice(priceString: string | number | undefined | null, options?: { currency?: string; locale?: string; }): string;
|
|
3215
3288
|
function getProductPrice(product: Pick<Product, 'basePrice' | 'salePrice'>): number;
|
|
3216
|
-
function getProductPriceInfo(product: Pick<Product, 'basePrice' | 'salePrice'>): {
|
|
3289
|
+
function getProductPriceInfo(product: Pick<Product, 'basePrice' | 'salePrice' | 'discount' | 'priceMin' | 'priceVaries'>): {
|
|
3217
3290
|
price: number; originalPrice: number; isOnSale: boolean; discountAmount: number; discountPercent: number;
|
|
3218
|
-
};
|
|
3291
|
+
}; // Falls back to priceMin when basePrice=0 (VARIABLE products)
|
|
3219
3292
|
function getVariantPrice(variant: Pick<ProductVariant, 'price' | 'salePrice'>, productBasePrice: string): number;
|
|
3220
3293
|
|
|
3221
3294
|
// Cart helpers
|
|
@@ -3328,6 +3401,37 @@ interface CartBundleOffer {
|
|
|
3328
3401
|
totalOriginalPrice: string;
|
|
3329
3402
|
totalDiscountedPrice: string;
|
|
3330
3403
|
}`;
|
|
3404
|
+
var REVIEWS_TYPES = `// ---- Product Reviews ----
|
|
3405
|
+
|
|
3406
|
+
interface ProductReview {
|
|
3407
|
+
id: string;
|
|
3408
|
+
productId: string;
|
|
3409
|
+
authorName: string; // max 100 chars
|
|
3410
|
+
rating: number; // integer 1-5
|
|
3411
|
+
body: string | null; // plain text, max 5000 chars
|
|
3412
|
+
verifiedPurchase: boolean; // derived server-side from DELIVERED orders
|
|
3413
|
+
hiddenAt?: string | null; // ISO datetime; admin responses only \u2014 null = visible
|
|
3414
|
+
createdAt: string; // ISO datetime
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
interface ProductReviewAdmin extends ProductReview {
|
|
3418
|
+
customerId: string | null;
|
|
3419
|
+
authorEmail: string | null;
|
|
3420
|
+
orderId: string | null;
|
|
3421
|
+
updatedAt: string;
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
interface SubmitProductReviewInput {
|
|
3425
|
+
authorName: string;
|
|
3426
|
+
authorEmail?: string; // recommended \u2014 used for guest dedupe + verified-purchase derivation
|
|
3427
|
+
rating: number; // 1-5
|
|
3428
|
+
body?: string; // optional
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
// Each Product carries denormalized review stats:
|
|
3432
|
+
// product.avgRating: number // 0 when reviewCount is 0
|
|
3433
|
+
// product.reviewCount: number // count of visible (hiddenAt IS NULL) reviews
|
|
3434
|
+
`;
|
|
3331
3435
|
var INQUIRIES_TYPES = `// ---- Contact Inquiries & Forms ----
|
|
3332
3436
|
|
|
3333
3437
|
// Two shapes \u2014 legacy and flexible. The two may be mixed; \`fields\` wins on key collision.
|
|
@@ -3571,6 +3675,7 @@ var TYPES_BY_DOMAIN = {
|
|
|
3571
3675
|
payments: PAYMENTS_TYPES,
|
|
3572
3676
|
helpers: HELPERS_TYPES,
|
|
3573
3677
|
inquiries: INQUIRIES_TYPES,
|
|
3678
|
+
reviews: REVIEWS_TYPES,
|
|
3574
3679
|
"modifier-groups": MODIFIER_GROUPS_TYPES
|
|
3575
3680
|
};
|
|
3576
3681
|
function getTypesByDomain(domain) {
|
|
@@ -3601,6 +3706,7 @@ var GET_TYPE_DEFINITIONS_SCHEMA = {
|
|
|
3601
3706
|
"payments",
|
|
3602
3707
|
"helpers",
|
|
3603
3708
|
"inquiries",
|
|
3709
|
+
"reviews",
|
|
3604
3710
|
"all"
|
|
3605
3711
|
]).describe(
|
|
3606
3712
|
'The domain of types to retrieve. Use "helpers" for helper function signatures and common types like StoreInfo, PaginatedResponse.'
|
|
@@ -3717,7 +3823,7 @@ function selectVariant(product: Product, selections: Record<string, string>) {
|
|
|
3717
3823
|
import { client } from './brainerce';
|
|
3718
3824
|
|
|
3719
3825
|
// 1. Submit the address + email. Email is REQUIRED on the DTO.
|
|
3720
|
-
const {
|
|
3826
|
+
const { checkout, rates } = await client.setShippingAddress(checkoutId, {
|
|
3721
3827
|
email,
|
|
3722
3828
|
firstName,
|
|
3723
3829
|
lastName,
|
|
@@ -3728,10 +3834,11 @@ const { shippingRates } = await client.setShippingAddress(checkoutId, {
|
|
|
3728
3834
|
postalCode,
|
|
3729
3835
|
country,
|
|
3730
3836
|
});
|
|
3837
|
+
// rates = available shipping rates; checkout = updated checkout object
|
|
3731
3838
|
|
|
3732
3839
|
// 2. Let the customer pick one of the returned rates.
|
|
3733
|
-
const chosen =
|
|
3734
|
-
await client.
|
|
3840
|
+
const chosen = rates[0]; // user selection
|
|
3841
|
+
await client.selectShippingMethod(checkoutId, chosen.id);
|
|
3735
3842
|
|
|
3736
3843
|
// 3. Re-fetch the checkout to get updated totals (tax may change after address).
|
|
3737
3844
|
const checkout = await client.getCheckout(checkoutId);
|
|
@@ -3785,21 +3892,21 @@ if (providers.length === 0) {
|
|
|
3785
3892
|
}
|
|
3786
3893
|
|
|
3787
3894
|
const active = providers[0];`,
|
|
3788
|
-
"checkout-stripe-confirm": `// Stripe confirm flow. Requires @stripe/stripe-js
|
|
3789
|
-
// (or the vanilla Stripe.js browser SDK).
|
|
3895
|
+
"checkout-stripe-confirm": `// Stripe confirm flow. Requires @stripe/stripe-js.
|
|
3790
3896
|
import { loadStripe } from '@stripe/stripe-js';
|
|
3791
3897
|
import { client } from './brainerce';
|
|
3792
3898
|
|
|
3793
3899
|
const stripe = await loadStripe(stripePublicKey);
|
|
3794
3900
|
if (!stripe) throw new Error('Stripe failed to load');
|
|
3795
3901
|
|
|
3796
|
-
//
|
|
3797
|
-
const
|
|
3798
|
-
|
|
3902
|
+
// Create a payment intent \u2014 returns clientSecret for Stripe Elements.
|
|
3903
|
+
const intent = await client.createPaymentIntent(checkoutId, {
|
|
3904
|
+
successUrl: \`\${window.location.origin}/order-confirmation?checkoutId=\${checkoutId}\`,
|
|
3905
|
+
cancelUrl: \`\${window.location.origin}/checkout?error=cancelled\`,
|
|
3799
3906
|
});
|
|
3800
3907
|
|
|
3801
|
-
// Use Stripe Elements to
|
|
3802
|
-
const result = await stripe.confirmCardPayment(clientSecret, {
|
|
3908
|
+
// Use Stripe Elements to confirm:
|
|
3909
|
+
const result = await stripe.confirmCardPayment(intent.clientSecret, {
|
|
3803
3910
|
payment_method: { card: cardElement },
|
|
3804
3911
|
});
|
|
3805
3912
|
|
|
@@ -3808,21 +3915,21 @@ if (result.error) {
|
|
|
3808
3915
|
return;
|
|
3809
3916
|
}
|
|
3810
3917
|
|
|
3811
|
-
// Success \u2014 redirect to
|
|
3918
|
+
// Success \u2014 redirect to confirmation page.
|
|
3812
3919
|
// The confirmation page runs handlePaymentSuccess + waitForOrder.
|
|
3813
3920
|
window.location.assign(\`/order-confirmation?checkoutId=\${checkoutId}\`);`,
|
|
3814
3921
|
"checkout-paypal-confirm": `// PayPal confirm flow. Use the PayPal JS SDK (render a PayPal button component).
|
|
3815
3922
|
import { client } from './brainerce';
|
|
3816
3923
|
|
|
3817
|
-
//
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
});
|
|
3924
|
+
// Create a payment intent first \u2014 for PayPal this returns the PayPal order ID.
|
|
3925
|
+
const intent = await client.createPaymentIntent(checkoutId, {
|
|
3926
|
+
successUrl: \`\${window.location.origin}/order-confirmation?checkoutId=\${checkoutId}\`,
|
|
3927
|
+
cancelUrl: \`\${window.location.origin}/checkout?error=cancelled\`,
|
|
3928
|
+
});
|
|
3823
3929
|
|
|
3824
|
-
|
|
3825
|
-
|
|
3930
|
+
// Render a PayPal button using intent.clientSdk (provider SDK config).
|
|
3931
|
+
// When the PayPal button's onApprove fires, redirect to confirmation:
|
|
3932
|
+
function onPayPalApprove() {
|
|
3826
3933
|
window.location.assign(\`/order-confirmation?checkoutId=\${checkoutId}\`);
|
|
3827
3934
|
}`,
|
|
3828
3935
|
"checkout-sandbox-confirm": `// Sandbox payment \u2014 store has sandboxPaymentsEnabled=true. No real charge.
|
|
@@ -3865,7 +3972,7 @@ async function registerAndMaybeVerify(input: {
|
|
|
3865
3972
|
firstName: string;
|
|
3866
3973
|
lastName: string;
|
|
3867
3974
|
}) {
|
|
3868
|
-
const result = await client.
|
|
3975
|
+
const result = await client.registerCustomer(input);
|
|
3869
3976
|
if (result.requiresVerification) {
|
|
3870
3977
|
// Route the user to your verify-email UI. Do NOT treat them as logged in.
|
|
3871
3978
|
return { state: 'needs-verification' as const };
|
|
@@ -3886,7 +3993,7 @@ async function resendCode() {
|
|
|
3886
3993
|
import { client } from './brainerce';
|
|
3887
3994
|
|
|
3888
3995
|
async function login(email: string, password: string) {
|
|
3889
|
-
const result = await client.
|
|
3996
|
+
const result = await client.loginCustomer(email, password);
|
|
3890
3997
|
if (result.requiresVerification) {
|
|
3891
3998
|
return { state: 'needs-verification' as const };
|
|
3892
3999
|
}
|
|
@@ -3921,25 +4028,37 @@ async function submitReset(token: string, newPassword: string) {
|
|
|
3921
4028
|
"oauth-redirect-and-callback": `// OAuth sign-in. Redirect flow, NOT popup.
|
|
3922
4029
|
import { client } from './brainerce';
|
|
3923
4030
|
|
|
3924
|
-
//
|
|
3925
|
-
const providers = await client.getAvailableOAuthProviders();
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
4031
|
+
// Step 1: Get available provider names (strings: 'GOOGLE', 'FACEBOOK', 'GITHUB')
|
|
4032
|
+
const { providers } = await client.getAvailableOAuthProviders();
|
|
4033
|
+
|
|
4034
|
+
// Step 2: For each provider, fetch the authorization URL, then redirect.
|
|
4035
|
+
// Call this when the user clicks a provider button:
|
|
4036
|
+
async function redirectToOAuth(provider: string) {
|
|
4037
|
+
const { authorizationUrl } = await client.getOAuthAuthorizeUrl(provider, {
|
|
4038
|
+
redirectUrl: \`\${window.location.origin}/auth/callback\`,
|
|
4039
|
+
});
|
|
4040
|
+
window.location.href = authorizationUrl; // full-page redirect, NOT a popup
|
|
3929
4041
|
}
|
|
3930
4042
|
|
|
3931
|
-
// On your
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
4043
|
+
// Step 3: On your /auth/callback route, read token from URL params:
|
|
4044
|
+
const params = new URLSearchParams(window.location.search);
|
|
4045
|
+
const oauthError = params.get('oauth_error');
|
|
4046
|
+
const token = params.get('token');
|
|
4047
|
+
if (oauthError) {
|
|
4048
|
+
window.location.href = '/login?error=' + encodeURIComponent(oauthError);
|
|
4049
|
+
} else if (token) {
|
|
4050
|
+
client.setCustomerToken(token);
|
|
4051
|
+
window.location.href = '/account';
|
|
4052
|
+
}`,
|
|
3937
4053
|
"coupon-apply-and-remove": `// Coupon input. Build the UI even if no coupons exist today \u2014 it auto-hides.
|
|
4054
|
+
// IMPORTANT: use applyCheckoutCoupon when a checkoutId is available (checkout page).
|
|
4055
|
+
// Using applyCoupon after checkout creation does NOT update the checkout total.
|
|
3938
4056
|
import { client } from './brainerce';
|
|
3939
4057
|
|
|
3940
|
-
|
|
4058
|
+
// On cart page (no checkout session yet)
|
|
4059
|
+
async function applyCouponToCart(cartId: string, code: string) {
|
|
3941
4060
|
try {
|
|
3942
|
-
const cart = await client.applyCoupon(code);
|
|
4061
|
+
const cart = await client.applyCoupon(cartId, code);
|
|
3943
4062
|
return { ok: true, cart };
|
|
3944
4063
|
} catch (err) {
|
|
3945
4064
|
// Invalid / expired / minimum not met. Surface the specific error.
|
|
@@ -3947,17 +4066,30 @@ async function applyCoupon(code: string) {
|
|
|
3947
4066
|
}
|
|
3948
4067
|
}
|
|
3949
4068
|
|
|
3950
|
-
async function
|
|
3951
|
-
|
|
3952
|
-
|
|
4069
|
+
async function removeCouponFromCart(cartId: string) {
|
|
4070
|
+
return client.removeCoupon(cartId);
|
|
4071
|
+
}
|
|
4072
|
+
|
|
4073
|
+
// On checkout page (checkout session already exists \u2014 always prefer this)
|
|
4074
|
+
async function applyCouponToCheckout(checkoutId: string, code: string) {
|
|
4075
|
+
try {
|
|
4076
|
+
const checkout = await client.applyCheckoutCoupon(checkoutId, code);
|
|
4077
|
+
return { ok: true, checkout }; // checkout.total is already updated
|
|
4078
|
+
} catch (err) {
|
|
4079
|
+
return { ok: false, error: (err as Error).message };
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
async function removeCouponFromCheckout(checkoutId: string) {
|
|
4084
|
+
return client.removeCheckoutCoupon(checkoutId);
|
|
3953
4085
|
}`,
|
|
3954
4086
|
"reservation-countdown": `// Reservation countdown. Use the SDK's expiry timestamp \u2014 do NOT invent
|
|
3955
4087
|
// your own timer state.
|
|
3956
4088
|
import { client } from './brainerce';
|
|
3957
4089
|
|
|
3958
|
-
function getRemainingMs(cart: {
|
|
3959
|
-
if (!cart.
|
|
3960
|
-
const expiry = new Date(cart.
|
|
4090
|
+
function getRemainingMs(cart: { reservation?: { expiresAt?: string } }): number {
|
|
4091
|
+
if (!cart.reservation?.expiresAt) return 0;
|
|
4092
|
+
const expiry = new Date(cart.reservation.expiresAt).getTime();
|
|
3961
4093
|
return Math.max(0, expiry - Date.now());
|
|
3962
4094
|
}
|
|
3963
4095
|
|
|
@@ -3984,8 +4116,8 @@ function onSearchChange(query: string, onResults: (items: unknown[]) => void) {
|
|
|
3984
4116
|
return;
|
|
3985
4117
|
}
|
|
3986
4118
|
searchTimer = setTimeout(async () => {
|
|
3987
|
-
const
|
|
3988
|
-
onResults(
|
|
4119
|
+
const { products } = await client.getSearchSuggestions(query);
|
|
4120
|
+
onResults(products);
|
|
3989
4121
|
}, 300);
|
|
3990
4122
|
}`,
|
|
3991
4123
|
"i18n-set-locale": `// i18n \u2014 only if get-store-capabilities reports i18n.enabled.
|
|
@@ -4342,19 +4474,13 @@ import { client } from './brainerce-client';
|
|
|
4342
4474
|
|
|
4343
4475
|
interface CheckoutOptions {
|
|
4344
4476
|
cartId: string;
|
|
4345
|
-
shippingAddress: object;
|
|
4346
|
-
shippingMethodId: string;
|
|
4347
|
-
paymentProviderId: string;
|
|
4348
4477
|
}
|
|
4349
4478
|
|
|
4350
4479
|
async function createCheckoutSafe(opts: CheckoutOptions) {
|
|
4351
4480
|
try {
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
shippingMethodId: opts.shippingMethodId,
|
|
4356
|
-
paymentProviderId: opts.paymentProviderId,
|
|
4357
|
-
});
|
|
4481
|
+
// createCheckout only takes cartId (+ optional customerId, selectedItemIds).
|
|
4482
|
+
// Shipping address, shipping method, and payment are set via separate calls.
|
|
4483
|
+
return await client.createCheckout({ cartId: opts.cartId });
|
|
4358
4484
|
} catch (err: unknown) {
|
|
4359
4485
|
if (!isApiError(err, 'PRICE_DRIFT')) throw err;
|
|
4360
4486
|
|
|
@@ -4484,6 +4610,93 @@ function renderCartItem(item: CartItem): string {
|
|
|
4484
4610
|
// multiply unitPrice \xD7 quantity directly.
|
|
4485
4611
|
function lineTotal(item: CartItem): number {
|
|
4486
4612
|
return item.unitPrice * item.quantity;
|
|
4613
|
+
}`,
|
|
4614
|
+
"product-reviews": `// PDP: list visible reviews + submit form + JSON-LD aggregateRating
|
|
4615
|
+
import { client } from '@/lib/brainerce';
|
|
4616
|
+
import type { Product, ProductReview, SubmitProductReviewInput } from 'brainerce';
|
|
4617
|
+
|
|
4618
|
+
export async function ProductReviewsSection({ product }: { product: Product }) {
|
|
4619
|
+
const { data } = await client.listProductReviews(product.id, { page: 1, limit: 20 });
|
|
4620
|
+
|
|
4621
|
+
return (
|
|
4622
|
+
<section>
|
|
4623
|
+
<h2>Reviews ({product.reviewCount ?? 0})</h2>
|
|
4624
|
+
{data.length === 0 && <p>No reviews yet \u2014 be the first.</p>}
|
|
4625
|
+
{data.map((r) => (
|
|
4626
|
+
<article key={r.id}>
|
|
4627
|
+
<strong>{r.authorName}</strong>
|
|
4628
|
+
{r.verifiedPurchase && <span> \xB7 Verified</span>}
|
|
4629
|
+
<div>{'\u2605'.repeat(r.rating)}{'\u2606'.repeat(5 - r.rating)}</div>
|
|
4630
|
+
{r.body && <p>{r.body}</p>}
|
|
4631
|
+
</article>
|
|
4632
|
+
))}
|
|
4633
|
+
<ReviewForm productId={product.id} />
|
|
4634
|
+
</section>
|
|
4635
|
+
);
|
|
4636
|
+
}
|
|
4637
|
+
|
|
4638
|
+
// JSON-LD \u2014 emit aggregateRating only when there is data
|
|
4639
|
+
function productJsonLd(product: Product, url: string) {
|
|
4640
|
+
return {
|
|
4641
|
+
'@context': 'https://schema.org',
|
|
4642
|
+
'@type': 'Product',
|
|
4643
|
+
name: product.name,
|
|
4644
|
+
image: product.images?.[0]?.url,
|
|
4645
|
+
url,
|
|
4646
|
+
sku: product.sku ?? product.id,
|
|
4647
|
+
...(product.reviewCount && product.reviewCount > 0 && {
|
|
4648
|
+
aggregateRating: {
|
|
4649
|
+
'@type': 'AggregateRating',
|
|
4650
|
+
ratingValue: product.avgRating,
|
|
4651
|
+
reviewCount: product.reviewCount,
|
|
4652
|
+
bestRating: 5,
|
|
4653
|
+
worstRating: 1,
|
|
4654
|
+
},
|
|
4655
|
+
}),
|
|
4656
|
+
};
|
|
4657
|
+
}
|
|
4658
|
+
|
|
4659
|
+
// Client-side form
|
|
4660
|
+
'use client';
|
|
4661
|
+
import { useState } from 'react';
|
|
4662
|
+
|
|
4663
|
+
function ReviewForm({ productId }: { productId: string }) {
|
|
4664
|
+
const [rating, setRating] = useState(5);
|
|
4665
|
+
const [body, setBody] = useState('');
|
|
4666
|
+
const [name, setName] = useState('');
|
|
4667
|
+
const [email, setEmail] = useState('');
|
|
4668
|
+
const [error, setError] = useState<string | null>(null);
|
|
4669
|
+
|
|
4670
|
+
async function submit(e: React.FormEvent) {
|
|
4671
|
+
e.preventDefault();
|
|
4672
|
+
setError(null);
|
|
4673
|
+
try {
|
|
4674
|
+
await client.submitProductReview(productId, {
|
|
4675
|
+
authorName: name,
|
|
4676
|
+
authorEmail: email,
|
|
4677
|
+
rating,
|
|
4678
|
+
body: body || undefined,
|
|
4679
|
+
});
|
|
4680
|
+
window.location.reload();
|
|
4681
|
+
} catch (err: any) {
|
|
4682
|
+
if (err?.status === 409) setError("You've already reviewed this product.");
|
|
4683
|
+
else if (err?.status === 429) setError('Too many submissions. Please wait a moment.');
|
|
4684
|
+
else setError('Could not submit review. Please try again.');
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4687
|
+
|
|
4688
|
+
return (
|
|
4689
|
+
<form onSubmit={submit}>
|
|
4690
|
+
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="Your name" required maxLength={100} />
|
|
4691
|
+
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
|
|
4692
|
+
<select value={rating} onChange={(e) => setRating(Number(e.target.value))}>
|
|
4693
|
+
{[5,4,3,2,1].map(n => <option key={n} value={n}>{'\u2605'.repeat(n)}</option>)}
|
|
4694
|
+
</select>
|
|
4695
|
+
<textarea value={body} onChange={(e) => setBody(e.target.value)} maxLength={5000} placeholder="Tell us what you think (optional)" />
|
|
4696
|
+
<button type="submit">Submit review</button>
|
|
4697
|
+
{error && <p role="alert">{error}</p>}
|
|
4698
|
+
</form>
|
|
4699
|
+
);
|
|
4487
4700
|
}`
|
|
4488
4701
|
};
|
|
4489
4702
|
async function handleGetCodeExample(args) {
|
|
@@ -5029,21 +5242,21 @@ var FLOWS = {
|
|
|
5029
5242
|
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\`.
|
|
5030
5243
|
2. **Submit the address to get shipping rates.**
|
|
5031
5244
|
\`\`\`ts
|
|
5032
|
-
const {
|
|
5245
|
+
const { checkout, rates } = await client.setShippingAddress(checkoutId, {
|
|
5033
5246
|
email,
|
|
5034
5247
|
firstName,
|
|
5035
5248
|
lastName,
|
|
5036
5249
|
line1, line2, city, region, postalCode, country,
|
|
5037
5250
|
});
|
|
5038
5251
|
\`\`\`
|
|
5039
|
-
|
|
5252
|
+
\`rates\` = available shipping rates for that address + the store's zones. \`checkout\` = updated checkout object.
|
|
5040
5253
|
3. **Let the customer pick a rate**, then persist the selection:
|
|
5041
5254
|
\`\`\`ts
|
|
5042
|
-
await client.
|
|
5255
|
+
await client.selectShippingMethod(checkoutId, rateId);
|
|
5043
5256
|
\`\`\`
|
|
5044
|
-
4. **Fetch payment providers
|
|
5257
|
+
4. **Fetch payment providers:**
|
|
5045
5258
|
\`\`\`ts
|
|
5046
|
-
const providers = await client.getPaymentProviders(
|
|
5259
|
+
const providers = await client.getPaymentProviders();
|
|
5047
5260
|
\`\`\`
|
|
5048
5261
|
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.
|
|
5049
5262
|
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.
|
|
@@ -5059,28 +5272,28 @@ Never bypass these steps. Never call \`submitGuestOrder\` / \`createOrder\` \u20
|
|
|
5059
5272
|
"auth-register": {
|
|
5060
5273
|
title: "Registration",
|
|
5061
5274
|
body: `1. **Collect** email, password, first name, last name. Enforce strong passwords client-side: 8+ chars, upper, lower, number, special.
|
|
5062
|
-
2. **Call
|
|
5275
|
+
2. **Call registerCustomer:**
|
|
5063
5276
|
\`\`\`ts
|
|
5064
|
-
const result = await client.
|
|
5277
|
+
const result = await client.registerCustomer({ email, password, firstName, lastName });
|
|
5065
5278
|
\`\`\`
|
|
5066
5279
|
3. **Branch on \`result.requiresVerification\`:**
|
|
5067
|
-
- If \`true\`: route the user to your verify-email UI. Do NOT treat them as logged in yet.
|
|
5068
|
-
- If \`false\`:
|
|
5280
|
+
- 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.
|
|
5281
|
+
- If \`false\`: call \`client.setCustomerToken(result.token)\` and route to the account area.
|
|
5069
5282
|
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()\`.
|
|
5070
|
-
5. **After verifyEmail resolves:** the user is now logged in
|
|
5283
|
+
5. **After verifyEmail resolves:** call \`client.setCustomerToken(result.token)\` \u2014 the user is now logged in \u2014 then route to the account area.
|
|
5071
5284
|
|
|
5072
5285
|
Build the verify-email step EVEN IF the store currently has verification disabled. It auto-hides; store owners enable it later.`
|
|
5073
5286
|
},
|
|
5074
5287
|
"auth-login": {
|
|
5075
5288
|
title: "Login",
|
|
5076
5289
|
body: `1. **Collect** email + password.
|
|
5077
|
-
2. **Call
|
|
5290
|
+
2. **Call loginCustomer:**
|
|
5078
5291
|
\`\`\`ts
|
|
5079
|
-
const result = await client.
|
|
5292
|
+
const result = await client.loginCustomer(email, password);
|
|
5080
5293
|
\`\`\`
|
|
5081
5294
|
3. **Branch on \`result.requiresVerification\`:**
|
|
5082
5295
|
- If \`true\`: route to verify-email. The user must complete verification before accessing account features.
|
|
5083
|
-
- If \`false\`:
|
|
5296
|
+
- If \`false\`: call \`client.setCustomerToken(result.token)\` and route to the previous page (or account area).
|
|
5084
5297
|
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.
|
|
5085
5298
|
5. **On error**, render the specific message (invalid credentials, rate limited, account disabled) \u2014 never swallow.`
|
|
5086
5299
|
},
|
|
@@ -5105,15 +5318,24 @@ Build both steps EVEN IF the store has no email provider configured today \u2014
|
|
|
5105
5318
|
},
|
|
5106
5319
|
oauth: {
|
|
5107
5320
|
title: "OAuth sign-in",
|
|
5108
|
-
body: `1. **
|
|
5321
|
+
body: `1. **Get available provider names:**
|
|
5109
5322
|
\`\`\`ts
|
|
5110
|
-
const providers = await client.getAvailableOAuthProviders();
|
|
5323
|
+
const { providers } = await client.getAvailableOAuthProviders();
|
|
5324
|
+
// providers = ['GOOGLE', 'FACEBOOK', 'GITHUB'] (strings, not objects with authorizationUrl)
|
|
5325
|
+
\`\`\`
|
|
5326
|
+
2. **For each provider, fetch the authorization URL:**
|
|
5327
|
+
\`\`\`ts
|
|
5328
|
+
const { authorizationUrl } = await client.getOAuthAuthorizeUrl(provider, {
|
|
5329
|
+
redirectUrl: \`\${window.location.origin}/auth/callback\`,
|
|
5330
|
+
});
|
|
5331
|
+
window.location.href = authorizationUrl; // full-page redirect, NOT a popup
|
|
5111
5332
|
\`\`\`
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5333
|
+
3. **On the callback page** the URL contains \`token\` + \`oauth_success\` (or \`oauth_error\`) query params. Extract and apply the token:
|
|
5334
|
+
\`\`\`ts
|
|
5335
|
+
const token = new URLSearchParams(location.search).get('token');
|
|
5336
|
+
if (token) client.setCustomerToken(token); // then redirect to account
|
|
5337
|
+
\`\`\`
|
|
5338
|
+
4. **On \`oauth_error\`:** redirect to login with an error message.
|
|
5117
5339
|
|
|
5118
5340
|
Build the OAuth button region AND the callback handler even when no providers are configured. They auto-hide.`
|
|
5119
5341
|
},
|
|
@@ -5135,7 +5357,7 @@ Build the OAuth button region AND the callback handler even when no providers ar
|
|
|
5135
5357
|
|
|
5136
5358
|
- **Cart ID persistence:** the SDK stores the cart ID across reloads. You do not need to write cart-to-localStorage code yourself.
|
|
5137
5359
|
- **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).
|
|
5138
|
-
- **Writes:** use \`client.addToCart\`, \`client.updateCartItem\`, \`client.removeCartItem\`, \`client.applyCoupon\`, \`client.removeCoupon\`. After each mutation the SDK returns the updated cart.
|
|
5360
|
+
- **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.
|
|
5139
5361
|
- **Totals:** call \`getCartTotals(cart)\` \u2014 do NOT read \`cart.total\`. The helper understands taxes, shipping, and discounts.
|
|
5140
5362
|
- **\`smartGetCart()\`** returns \`CartWithIncludes\` (extends \`Cart\`). All carts are server-side. Pass \`{ include: ['recommendations', 'upgrades', 'bundles'] }\` to fetch extras in one request.
|
|
5141
5363
|
- **NEVER mutate cart state outside SDK helpers.** Any hand-rolled cart update risks desync with the reservation timer and the checkout flow.`
|
|
@@ -5267,7 +5489,7 @@ var FEATURES = [
|
|
|
5267
5489
|
id: "browse-products",
|
|
5268
5490
|
title: "Browse, filter, and search products",
|
|
5269
5491
|
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.",
|
|
5270
|
-
sdk: "client.getProducts({ page, limit, filters, sort, metafields }), client.getPublicMetafieldDefinitions(), client.
|
|
5492
|
+
sdk: "client.getProducts({ page, limit, filters, sort, metafields }), client.getPublicMetafieldDefinitions(), client.getSearchSuggestions(query)",
|
|
5271
5493
|
mandatory: "mandatory"
|
|
5272
5494
|
},
|
|
5273
5495
|
{
|
|
@@ -5277,6 +5499,13 @@ var FEATURES = [
|
|
|
5277
5499
|
sdk: "client.getProductBySlug(slug). Helpers: getProductPriceInfo, getVariantPrice, getStockStatus, getVariantOptions, getProductSwatches, getDescriptionContent, getProductMetafieldValue.",
|
|
5278
5500
|
mandatory: "mandatory"
|
|
5279
5501
|
},
|
|
5502
|
+
{
|
|
5503
|
+
id: "product-reviews",
|
|
5504
|
+
title: "Display + collect product reviews with stars + JSON-LD",
|
|
5505
|
+
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.',
|
|
5506
|
+
sdk: "client.listProductReviews(productId), client.submitProductReview(productId, { authorName, authorEmail, rating, body }). Product.avgRating + Product.reviewCount are denormalized rollups returned on every product fetch.",
|
|
5507
|
+
mandatory: "mandatory"
|
|
5508
|
+
},
|
|
5280
5509
|
{
|
|
5281
5510
|
id: "product-customization-fields",
|
|
5282
5511
|
title: "Render buyer-input customization fields on the product page",
|
|
@@ -5297,7 +5526,7 @@ var FEATURES = [
|
|
|
5297
5526
|
id: "cart-coupons",
|
|
5298
5527
|
title: "Apply and remove coupon codes",
|
|
5299
5528
|
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.",
|
|
5300
|
-
sdk: "client.applyCoupon(code), client.removeCoupon()",
|
|
5529
|
+
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)",
|
|
5301
5530
|
mandatory: "mandatory",
|
|
5302
5531
|
capabilityFlag: "hasCoupons",
|
|
5303
5532
|
whenDisabledNote: "Store has no coupons configured today. Build the UI anyway \u2014 it auto-hides until the store owner creates a coupon."
|
|
@@ -5314,7 +5543,7 @@ var FEATURES = [
|
|
|
5314
5543
|
id: "checkout",
|
|
5315
5544
|
title: "Complete a full checkout end-to-end",
|
|
5316
5545
|
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.",
|
|
5317
|
-
sdk: "client.setShippingAddress, client.
|
|
5546
|
+
sdk: "client.setShippingAddress (returns { checkout, rates }), client.selectShippingMethod, client.getPaymentProviders(), provider-specific confirm, client.handlePaymentSuccess, client.waitForOrder",
|
|
5318
5547
|
flowRef: "checkout",
|
|
5319
5548
|
mandatory: "mandatory"
|
|
5320
5549
|
},
|
|
@@ -5330,7 +5559,7 @@ var FEATURES = [
|
|
|
5330
5559
|
id: "register",
|
|
5331
5560
|
title: "Register a new account with email verification",
|
|
5332
5561
|
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.',
|
|
5333
|
-
sdk: "client.
|
|
5562
|
+
sdk: "client.registerCustomer({email, password, firstName, lastName}), client.verifyEmail(code), client.resendVerificationEmail()",
|
|
5334
5563
|
flowRef: "auth-register",
|
|
5335
5564
|
mandatory: "mandatory",
|
|
5336
5565
|
capabilityFlag: "oauth",
|
|
@@ -5340,7 +5569,7 @@ var FEATURES = [
|
|
|
5340
5569
|
id: "login",
|
|
5341
5570
|
title: "Log in with email / password and handle verification branch",
|
|
5342
5571
|
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).",
|
|
5343
|
-
sdk: "client.
|
|
5572
|
+
sdk: "client.loginCustomer(email, password)",
|
|
5344
5573
|
flowRef: "auth-login",
|
|
5345
5574
|
mandatory: "mandatory"
|
|
5346
5575
|
},
|
|
@@ -5373,14 +5602,14 @@ var FEATURES = [
|
|
|
5373
5602
|
id: "header",
|
|
5374
5603
|
title: "Global header with cart count and search",
|
|
5375
5604
|
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.",
|
|
5376
|
-
sdk: "client.getCart() for the count, client.
|
|
5605
|
+
sdk: "client.getCart() for the count, client.getSearchSuggestions(query) for autocomplete",
|
|
5377
5606
|
mandatory: "mandatory"
|
|
5378
5607
|
},
|
|
5379
5608
|
{
|
|
5380
5609
|
id: "discount-banners",
|
|
5381
5610
|
title: "Show active discount banners and badges",
|
|
5382
5611
|
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.",
|
|
5383
|
-
sdk: "client.getDiscountBanners(), getProductDiscountBadge(
|
|
5612
|
+
sdk: "client.getDiscountBanners(), client.getProductDiscountBadge(productId)",
|
|
5384
5613
|
mandatory: "mandatory",
|
|
5385
5614
|
capabilityFlag: "hasDiscountRules",
|
|
5386
5615
|
whenDisabledNote: "Store has no discount rules active. Build the banner and badge components anyway \u2014 they auto-hide."
|