@brainerce/mcp-server 2.7.0 → 2.8.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 +372 -10
- package/dist/bin/stdio.js +372 -10
- package/dist/index.d.mts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +372 -10
- package/dist/index.mjs +372 -10
- package/package.json +1 -1
package/dist/bin/http.js
CHANGED
|
@@ -411,6 +411,234 @@ function PayPalPaymentForm({ clientId, orderId, checkoutId }: { clientId: string
|
|
|
411
411
|
}
|
|
412
412
|
\`\`\``;
|
|
413
413
|
}
|
|
414
|
+
function getCheckoutCustomFieldsSection() {
|
|
415
|
+
return `## Checkout Custom Fields (Optional Step Between Shipping & Payment)
|
|
416
|
+
|
|
417
|
+
Some stores configure **custom fields** at checkout \u2014 e.g., gift wrapping, floor number for delivery, installation date, custom engraving. Each field can add a **surcharge** to the order total. The server filters by visibility (always / shipping-only / pickup-only / cart-products) and calculates surcharges automatically.
|
|
418
|
+
|
|
419
|
+
### When to Build This
|
|
420
|
+
|
|
421
|
+
Check capabilities first:
|
|
422
|
+
\`\`\`typescript
|
|
423
|
+
const caps = await fetch(\`/api/vc/\${connectionId}/capabilities\`).then(r => r.json());
|
|
424
|
+
if (caps.features.hasCheckoutCustomFields) {
|
|
425
|
+
// Build the step described below
|
|
426
|
+
}
|
|
427
|
+
\`\`\`
|
|
428
|
+
|
|
429
|
+
If \`hasCheckoutCustomFields\` is true, you MUST build this step or customers will be charged surcharges they never saw.
|
|
430
|
+
|
|
431
|
+
### Where to Place It
|
|
432
|
+
|
|
433
|
+
After \`selectShippingMethod()\` (or pickup confirmation), BEFORE \`createPaymentIntent()\`. Pseudocode:
|
|
434
|
+
|
|
435
|
+
\`\`\`
|
|
436
|
+
address \u2192 shipping/pickup \u2192 custom-fields (NEW) \u2192 payment
|
|
437
|
+
\`\`\`
|
|
438
|
+
|
|
439
|
+
If the loaded fields array is empty for this checkout (visibility filters can hide all fields), **skip the step entirely** \u2014 go straight to payment with no UI flicker.
|
|
440
|
+
|
|
441
|
+
### Loading Field Definitions
|
|
442
|
+
|
|
443
|
+
\`\`\`typescript
|
|
444
|
+
import type { CheckoutCustomFieldDefinition } from 'brainerce';
|
|
445
|
+
|
|
446
|
+
const fields = await client.getCheckoutCustomFields(checkoutId);
|
|
447
|
+
if (fields.length === 0) {
|
|
448
|
+
setStep('payment'); // Nothing to collect
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
\`\`\`
|
|
452
|
+
|
|
453
|
+
Each \`CheckoutCustomFieldDefinition\` has:
|
|
454
|
+
- \`key\` \u2014 the form field name to use in your state
|
|
455
|
+
- \`name\` \u2014 display label
|
|
456
|
+
- \`description\` \u2014 helper text (render below the input)
|
|
457
|
+
- \`type\` \u2014 \`'TEXT' | 'TEXTAREA' | 'NUMBER' | 'BOOLEAN' | 'SELECT' | 'DATE'\`
|
|
458
|
+
- \`required\` \u2014 boolean
|
|
459
|
+
- \`minLength\` / \`maxLength\` \u2014 for TEXT/TEXTAREA
|
|
460
|
+
- \`minValue\` / \`maxValue\` \u2014 for NUMBER
|
|
461
|
+
- \`options\` \u2014 \`Array<{value, label}>\` for SELECT
|
|
462
|
+
- \`pricing\` \u2014 never render this; the server uses it to compute surcharges
|
|
463
|
+
|
|
464
|
+
### Rendering All 6 Field Types
|
|
465
|
+
|
|
466
|
+
\`\`\`typescript
|
|
467
|
+
function CustomFieldsStep({ fields, values, onChange, onApply, loading }: {
|
|
468
|
+
fields: CheckoutCustomFieldDefinition[];
|
|
469
|
+
values: Record<string, unknown>;
|
|
470
|
+
onChange: (key: string, value: unknown) => void;
|
|
471
|
+
onApply: () => void;
|
|
472
|
+
loading?: boolean;
|
|
473
|
+
}) {
|
|
474
|
+
return (
|
|
475
|
+
<div className="space-y-4">
|
|
476
|
+
{fields.map((field) => {
|
|
477
|
+
const value = values[field.key] ?? '';
|
|
478
|
+
const label = (
|
|
479
|
+
<label className="block text-sm font-medium">
|
|
480
|
+
{field.name}{field.required && <span className="text-red-500"> *</span>}
|
|
481
|
+
</label>
|
|
482
|
+
);
|
|
483
|
+
const help = field.description && (
|
|
484
|
+
<p className="text-xs text-gray-500 mt-1">{field.description}</p>
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
switch (field.type) {
|
|
488
|
+
case 'TEXT':
|
|
489
|
+
return (
|
|
490
|
+
<div key={field.key}>
|
|
491
|
+
{label}
|
|
492
|
+
<input
|
|
493
|
+
type="text"
|
|
494
|
+
value={value as string}
|
|
495
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
496
|
+
required={field.required}
|
|
497
|
+
minLength={field.minLength ?? undefined}
|
|
498
|
+
maxLength={field.maxLength ?? undefined}
|
|
499
|
+
className="w-full border rounded px-3 py-2"
|
|
500
|
+
/>
|
|
501
|
+
{help}
|
|
502
|
+
</div>
|
|
503
|
+
);
|
|
504
|
+
case 'TEXTAREA':
|
|
505
|
+
return (
|
|
506
|
+
<div key={field.key}>
|
|
507
|
+
{label}
|
|
508
|
+
<textarea
|
|
509
|
+
value={value as string}
|
|
510
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
511
|
+
required={field.required}
|
|
512
|
+
minLength={field.minLength ?? undefined}
|
|
513
|
+
maxLength={field.maxLength ?? undefined}
|
|
514
|
+
rows={3}
|
|
515
|
+
className="w-full border rounded px-3 py-2"
|
|
516
|
+
/>
|
|
517
|
+
{help}
|
|
518
|
+
</div>
|
|
519
|
+
);
|
|
520
|
+
case 'NUMBER':
|
|
521
|
+
return (
|
|
522
|
+
<div key={field.key}>
|
|
523
|
+
{label}
|
|
524
|
+
<input
|
|
525
|
+
type="number"
|
|
526
|
+
value={value as number}
|
|
527
|
+
onChange={(e) => onChange(field.key, e.target.valueAsNumber)}
|
|
528
|
+
required={field.required}
|
|
529
|
+
min={field.minValue ?? undefined}
|
|
530
|
+
max={field.maxValue ?? undefined}
|
|
531
|
+
className="w-full border rounded px-3 py-2"
|
|
532
|
+
/>
|
|
533
|
+
{help}
|
|
534
|
+
</div>
|
|
535
|
+
);
|
|
536
|
+
case 'BOOLEAN':
|
|
537
|
+
return (
|
|
538
|
+
<div key={field.key} className="flex items-start gap-2">
|
|
539
|
+
<input
|
|
540
|
+
type="checkbox"
|
|
541
|
+
checked={value === true}
|
|
542
|
+
onChange={(e) => onChange(field.key, e.target.checked)}
|
|
543
|
+
className="mt-1"
|
|
544
|
+
/>
|
|
545
|
+
<div>{label}{help}</div>
|
|
546
|
+
</div>
|
|
547
|
+
);
|
|
548
|
+
case 'SELECT':
|
|
549
|
+
return (
|
|
550
|
+
<div key={field.key}>
|
|
551
|
+
{label}
|
|
552
|
+
<select
|
|
553
|
+
value={value as string}
|
|
554
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
555
|
+
required={field.required}
|
|
556
|
+
className="w-full border rounded px-3 py-2"
|
|
557
|
+
>
|
|
558
|
+
<option value="">\u2014 Select \u2014</option>
|
|
559
|
+
{field.options?.map((opt) => (
|
|
560
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
561
|
+
))}
|
|
562
|
+
</select>
|
|
563
|
+
{help}
|
|
564
|
+
</div>
|
|
565
|
+
);
|
|
566
|
+
case 'DATE':
|
|
567
|
+
return (
|
|
568
|
+
<div key={field.key}>
|
|
569
|
+
{label}
|
|
570
|
+
<input
|
|
571
|
+
type="date"
|
|
572
|
+
value={value as string}
|
|
573
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
574
|
+
required={field.required}
|
|
575
|
+
className="w-full border rounded px-3 py-2"
|
|
576
|
+
/>
|
|
577
|
+
{help}
|
|
578
|
+
</div>
|
|
579
|
+
);
|
|
580
|
+
default:
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
})}
|
|
584
|
+
|
|
585
|
+
<button
|
|
586
|
+
type="button"
|
|
587
|
+
onClick={onApply}
|
|
588
|
+
disabled={loading}
|
|
589
|
+
className="w-full bg-black text-white py-3 rounded"
|
|
590
|
+
>
|
|
591
|
+
{loading ? 'Updating\u2026' : 'Continue to Payment'}
|
|
592
|
+
</button>
|
|
593
|
+
</div>
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
\`\`\`
|
|
597
|
+
|
|
598
|
+
### Submitting Values & Recalculating Surcharges
|
|
599
|
+
|
|
600
|
+
\`\`\`typescript
|
|
601
|
+
async function applyCustomFields() {
|
|
602
|
+
setLoading(true);
|
|
603
|
+
try {
|
|
604
|
+
// Server validates required + min/max + coerces types + recalculates totals
|
|
605
|
+
const updated = await client.setCheckoutCustomFields(checkoutId, customFieldValues);
|
|
606
|
+
setCheckout(updated); // updated.surchargeAmount and updated.appliedSurcharges are now fresh
|
|
607
|
+
setStep('payment');
|
|
608
|
+
} catch (err) {
|
|
609
|
+
setError(err instanceof Error ? err.message : 'Failed to save selections');
|
|
610
|
+
} finally {
|
|
611
|
+
setLoading(false);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
\`\`\`
|
|
615
|
+
|
|
616
|
+
### Order Summary \u2014 Show the Surcharges
|
|
617
|
+
|
|
618
|
+
**Critical:** if you don't render \`appliedSurcharges\`, customers will see a higher total with no explanation. Add this to your order summary above the total row:
|
|
619
|
+
|
|
620
|
+
\`\`\`typescript
|
|
621
|
+
{checkout.appliedSurcharges?.length > 0 && (
|
|
622
|
+
<>
|
|
623
|
+
{checkout.appliedSurcharges.map((s) => (
|
|
624
|
+
<div key={s.key} className="flex justify-between text-sm text-gray-600">
|
|
625
|
+
<span>{s.name}</span>
|
|
626
|
+
<span>{formatPrice(s.amount, { currency })}</span>
|
|
627
|
+
</div>
|
|
628
|
+
))}
|
|
629
|
+
</>
|
|
630
|
+
)}
|
|
631
|
+
\`\`\`
|
|
632
|
+
|
|
633
|
+
\`checkout.surchargeAmount\` is a string (use \`parseFloat()\`); \`checkout.totalAmount\` already includes it.
|
|
634
|
+
|
|
635
|
+
### Notes
|
|
636
|
+
|
|
637
|
+
- The server filters fields by \`visibility\` (\`always\` / \`shipping\` / \`pickup\` / \`products\`) \u2014 your client just renders what \`getCheckoutCustomFields()\` returns.
|
|
638
|
+
- The server validates \`required\`, \`minLength\`/\`maxLength\`, \`minValue\`/\`maxValue\`, and SELECT options. Don't rely on client validation alone \u2014 surface server errors with a toast.
|
|
639
|
+
- If \`getCheckoutCustomFields()\` returns 0 fields, skip the step entirely. Don't render an empty UI.
|
|
640
|
+
`;
|
|
641
|
+
}
|
|
414
642
|
function getPaymentProvidersSection() {
|
|
415
643
|
return `## Payment Provider Detection (DO THIS FIRST on checkout page!)
|
|
416
644
|
|
|
@@ -1260,6 +1488,10 @@ ${getCheckoutFlowSection(currency)}
|
|
|
1260
1488
|
|
|
1261
1489
|
---
|
|
1262
1490
|
|
|
1491
|
+
${getCheckoutCustomFieldsSection()}
|
|
1492
|
+
|
|
1493
|
+
---
|
|
1494
|
+
|
|
1263
1495
|
${getPaymentProvidersSection()}
|
|
1264
1496
|
|
|
1265
1497
|
---
|
|
@@ -1318,6 +1550,8 @@ function getSectionByTopic(topic, connectionId, currency) {
|
|
|
1318
1550
|
return getCartSection(cur);
|
|
1319
1551
|
case "checkout":
|
|
1320
1552
|
return getCheckoutFlowSection(cur);
|
|
1553
|
+
case "checkout-custom-fields":
|
|
1554
|
+
return getCheckoutCustomFieldsSection();
|
|
1321
1555
|
case "payment":
|
|
1322
1556
|
return getPaymentProvidersSection();
|
|
1323
1557
|
case "auth":
|
|
@@ -1346,7 +1580,7 @@ function getSectionByTopic(topic, connectionId, currency) {
|
|
|
1346
1580
|
storeType: "E-commerce store"
|
|
1347
1581
|
});
|
|
1348
1582
|
default:
|
|
1349
|
-
return `Unknown topic: "${topic}". Available topics: setup, products, cart, checkout, payment, auth, order-confirmation, inventory, discounts, recommendations, tax, critical-rules, type-reference, admin, all`;
|
|
1583
|
+
return `Unknown topic: "${topic}". Available topics: setup, products, cart, checkout, checkout-custom-fields, payment, auth, order-confirmation, inventory, discounts, recommendations, tax, critical-rules, type-reference, admin, all`;
|
|
1350
1584
|
}
|
|
1351
1585
|
}
|
|
1352
1586
|
|
|
@@ -1359,6 +1593,7 @@ var GET_SDK_DOCS_SCHEMA = {
|
|
|
1359
1593
|
"products",
|
|
1360
1594
|
"cart",
|
|
1361
1595
|
"checkout",
|
|
1596
|
+
"checkout-custom-fields",
|
|
1362
1597
|
"payment",
|
|
1363
1598
|
"auth",
|
|
1364
1599
|
"order-confirmation",
|
|
@@ -3380,6 +3615,7 @@ import type {
|
|
|
3380
3615
|
ShippingDestinations,
|
|
3381
3616
|
PickupLocation,
|
|
3382
3617
|
CheckoutBumpsResponse,
|
|
3618
|
+
CheckoutCustomFieldDefinition,
|
|
3383
3619
|
} from 'brainerce';
|
|
3384
3620
|
import { formatPrice } from 'brainerce';
|
|
3385
3621
|
import { getClient } from '@/lib/brainerce';
|
|
@@ -3389,6 +3625,7 @@ import { ShippingStep } from '@/components/checkout/shipping-step';
|
|
|
3389
3625
|
import { PaymentStep } from '@/components/checkout/payment-step';
|
|
3390
3626
|
import { DeliveryMethodStep } from '@/components/checkout/delivery-method-step';
|
|
3391
3627
|
import { PickupStep } from '@/components/checkout/pickup-step';
|
|
3628
|
+
import { CustomFieldsStep } from '@/components/checkout/custom-fields-step';
|
|
3392
3629
|
import { TaxDisplay } from '@/components/checkout/tax-display';
|
|
3393
3630
|
import { OrderBumpCard } from '@/components/checkout/order-bump-card';
|
|
3394
3631
|
import { CouponInput } from '@/components/cart/coupon-input';
|
|
@@ -3397,7 +3634,7 @@ import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
|
3397
3634
|
import { useTranslations } from '@/lib/translations';
|
|
3398
3635
|
import { cn } from '@/lib/utils';
|
|
3399
3636
|
|
|
3400
|
-
type CheckoutStep = 'method' | 'address' | 'shipping' | 'pickup' | 'payment';
|
|
3637
|
+
type CheckoutStep = 'method' | 'address' | 'shipping' | 'pickup' | 'custom-fields' | 'payment';
|
|
3401
3638
|
|
|
3402
3639
|
function CheckoutContent() {
|
|
3403
3640
|
const searchParams = useSearchParams();
|
|
@@ -3430,6 +3667,9 @@ function CheckoutContent() {
|
|
|
3430
3667
|
const [orderBumps, setOrderBumps] = useState<CheckoutBumpsResponse | null>(null);
|
|
3431
3668
|
const [addedBumpIds, setAddedBumpIds] = useState<Set<string>>(new Set());
|
|
3432
3669
|
const [bumpLoading, setBumpLoading] = useState<string | null>(null);
|
|
3670
|
+
const [customFields, setCustomFields] = useState<CheckoutCustomFieldDefinition[]>([]);
|
|
3671
|
+
const [customFieldValues, setCustomFieldValues] = useState<Record<string, unknown>>({});
|
|
3672
|
+
const [customFieldsLoading, setCustomFieldsLoading] = useState(false);
|
|
3433
3673
|
|
|
3434
3674
|
// Check for returning from canceled payment
|
|
3435
3675
|
const canceled = searchParams.get('canceled') === 'true';
|
|
@@ -3481,6 +3721,23 @@ function CheckoutContent() {
|
|
|
3481
3721
|
const existing = await client.getCheckout(existingCheckoutId);
|
|
3482
3722
|
setCheckout(existing);
|
|
3483
3723
|
|
|
3724
|
+
// Preload custom field definitions and any existing values so the
|
|
3725
|
+
// step indicator and "change options" affordance work on resume.
|
|
3726
|
+
client
|
|
3727
|
+
.getCheckoutCustomFields(existing.id)
|
|
3728
|
+
.then((fields) => {
|
|
3729
|
+
setCustomFields(fields);
|
|
3730
|
+
const existingValues = (
|
|
3731
|
+
existing as unknown as {
|
|
3732
|
+
customFieldValues?: Record<string, unknown> | null;
|
|
3733
|
+
}
|
|
3734
|
+
).customFieldValues;
|
|
3735
|
+
if (existingValues) setCustomFieldValues(existingValues);
|
|
3736
|
+
})
|
|
3737
|
+
.catch(() => {
|
|
3738
|
+
setCustomFields([]);
|
|
3739
|
+
});
|
|
3740
|
+
|
|
3484
3741
|
// Determine step based on checkout state
|
|
3485
3742
|
const allDigital = existing.lineItems.every(
|
|
3486
3743
|
(i) => (i.product as unknown as { isDownloadable?: boolean }).isDownloadable
|
|
@@ -3651,6 +3908,21 @@ function CheckoutContent() {
|
|
|
3651
3908
|
}
|
|
3652
3909
|
}
|
|
3653
3910
|
|
|
3911
|
+
// After shipping/pickup is set, decide whether to show the custom-fields step
|
|
3912
|
+
// or jump straight to payment. Returns the next step.
|
|
3913
|
+
async function loadCustomFieldsOrSkip(checkoutId: string): Promise<CheckoutStep> {
|
|
3914
|
+
try {
|
|
3915
|
+
const fields = await getClient().getCheckoutCustomFields(checkoutId);
|
|
3916
|
+
setCustomFields(fields);
|
|
3917
|
+
return fields.length > 0 ? 'custom-fields' : 'payment';
|
|
3918
|
+
} catch {
|
|
3919
|
+
// If the endpoint isn't available or fails, fall through to payment
|
|
3920
|
+
// rather than blocking the customer.
|
|
3921
|
+
setCustomFields([]);
|
|
3922
|
+
return 'payment';
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3654
3926
|
// Handle shipping method selection
|
|
3655
3927
|
async function handleShippingSelect(rateId: string) {
|
|
3656
3928
|
if (!checkout) return;
|
|
@@ -3663,7 +3935,7 @@ function CheckoutContent() {
|
|
|
3663
3935
|
|
|
3664
3936
|
const updated = await client.selectShippingMethod(checkout.id, rateId);
|
|
3665
3937
|
setCheckout(updated);
|
|
3666
|
-
setStep(
|
|
3938
|
+
setStep(await loadCustomFieldsOrSkip(updated.id));
|
|
3667
3939
|
} catch (err) {
|
|
3668
3940
|
const message = err instanceof Error ? err.message : t('failedToSelectShipping');
|
|
3669
3941
|
setError(message);
|
|
@@ -3672,6 +3944,23 @@ function CheckoutContent() {
|
|
|
3672
3944
|
}
|
|
3673
3945
|
}
|
|
3674
3946
|
|
|
3947
|
+
// Submit custom fields
|
|
3948
|
+
async function handleCustomFieldsApply() {
|
|
3949
|
+
if (!checkout) return;
|
|
3950
|
+
try {
|
|
3951
|
+
setCustomFieldsLoading(true);
|
|
3952
|
+
setError(null);
|
|
3953
|
+
const updated = await getClient().setCheckoutCustomFields(checkout.id, customFieldValues);
|
|
3954
|
+
setCheckout(updated);
|
|
3955
|
+
setStep('payment');
|
|
3956
|
+
} catch (err) {
|
|
3957
|
+
const message = err instanceof Error ? err.message : t('customFieldsFailed');
|
|
3958
|
+
setError(message);
|
|
3959
|
+
} finally {
|
|
3960
|
+
setCustomFieldsLoading(false);
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3675
3964
|
// Handle delivery method selection
|
|
3676
3965
|
async function handleDeliveryTypeSelect(method: 'shipping' | 'pickup') {
|
|
3677
3966
|
if (!checkout) return;
|
|
@@ -3717,7 +4006,7 @@ function CheckoutContent() {
|
|
|
3717
4006
|
phone: customerInfo.phone,
|
|
3718
4007
|
});
|
|
3719
4008
|
setCheckout(updated);
|
|
3720
|
-
setStep(
|
|
4009
|
+
setStep(await loadCustomFieldsOrSkip(updated.id));
|
|
3721
4010
|
} catch (err) {
|
|
3722
4011
|
const message = err instanceof Error ? err.message : t('failedToSelectPickup');
|
|
3723
4012
|
setError(message);
|
|
@@ -3779,9 +4068,15 @@ function CheckoutContent() {
|
|
|
3779
4068
|
);
|
|
3780
4069
|
}
|
|
3781
4070
|
|
|
4071
|
+
const customFieldsStep =
|
|
4072
|
+
customFields.length > 0
|
|
4073
|
+
? [{ key: 'custom-fields' as CheckoutStep, label: t('stepCustomFields') }]
|
|
4074
|
+
: [];
|
|
4075
|
+
|
|
3782
4076
|
const steps: { key: CheckoutStep; label: string }[] = isAllDigital
|
|
3783
4077
|
? [
|
|
3784
4078
|
{ key: 'address', label: t('stepContactInfo') },
|
|
4079
|
+
...customFieldsStep,
|
|
3785
4080
|
{ key: 'payment', label: t('stepPayment') },
|
|
3786
4081
|
]
|
|
3787
4082
|
: pickupLocations.length > 0
|
|
@@ -3789,17 +4084,20 @@ function CheckoutContent() {
|
|
|
3789
4084
|
? [
|
|
3790
4085
|
{ key: 'method', label: t('stepMethod') },
|
|
3791
4086
|
{ key: 'pickup', label: t('stepPickup') },
|
|
4087
|
+
...customFieldsStep,
|
|
3792
4088
|
{ key: 'payment', label: t('stepPayment') },
|
|
3793
4089
|
]
|
|
3794
4090
|
: [
|
|
3795
4091
|
{ key: 'method', label: t('stepMethod') },
|
|
3796
4092
|
{ key: 'address', label: t('stepAddress') },
|
|
3797
4093
|
{ key: 'shipping', label: t('stepShipping') },
|
|
4094
|
+
...customFieldsStep,
|
|
3798
4095
|
{ key: 'payment', label: t('stepPayment') },
|
|
3799
4096
|
]
|
|
3800
4097
|
: [
|
|
3801
4098
|
{ key: 'address', label: t('stepAddress') },
|
|
3802
4099
|
{ key: 'shipping', label: t('stepShipping') },
|
|
4100
|
+
...customFieldsStep,
|
|
3803
4101
|
{ key: 'payment', label: t('stepPayment') },
|
|
3804
4102
|
];
|
|
3805
4103
|
|
|
@@ -3998,19 +4296,54 @@ function CheckoutContent() {
|
|
|
3998
4296
|
</div>
|
|
3999
4297
|
)}
|
|
4000
4298
|
|
|
4299
|
+
{/* Custom Fields (optional, between shipping/pickup and payment) */}
|
|
4300
|
+
{step === 'custom-fields' && checkout && (
|
|
4301
|
+
<div>
|
|
4302
|
+
<div className="mb-4 flex items-center justify-between">
|
|
4303
|
+
<h2 className="text-foreground text-lg font-semibold">{t('customFieldsTitle')}</h2>
|
|
4304
|
+
<button
|
|
4305
|
+
type="button"
|
|
4306
|
+
onClick={() => setStep(deliveryType === 'pickup' ? 'pickup' : 'shipping')}
|
|
4307
|
+
className="text-primary text-sm hover:underline"
|
|
4308
|
+
>
|
|
4309
|
+
{deliveryType === 'pickup' ? t('changePickup') : t('changeShipping')}
|
|
4310
|
+
</button>
|
|
4311
|
+
</div>
|
|
4312
|
+
<CustomFieldsStep
|
|
4313
|
+
fields={customFields}
|
|
4314
|
+
values={customFieldValues}
|
|
4315
|
+
onChange={(key, value) =>
|
|
4316
|
+
setCustomFieldValues((prev) => ({ ...prev, [key]: value }))
|
|
4317
|
+
}
|
|
4318
|
+
onApply={handleCustomFieldsApply}
|
|
4319
|
+
loading={customFieldsLoading}
|
|
4320
|
+
/>
|
|
4321
|
+
</div>
|
|
4322
|
+
)}
|
|
4323
|
+
|
|
4001
4324
|
{/* Payment */}
|
|
4002
4325
|
{step === 'payment' && checkout && (
|
|
4003
4326
|
<div>
|
|
4004
4327
|
<div className="mb-4 flex items-center justify-between">
|
|
4005
4328
|
<h2 className="text-foreground text-lg font-semibold">{t('payment')}</h2>
|
|
4006
|
-
{
|
|
4329
|
+
{customFields.length > 0 ? (
|
|
4007
4330
|
<button
|
|
4008
4331
|
type="button"
|
|
4009
|
-
onClick={() => setStep(
|
|
4332
|
+
onClick={() => setStep('custom-fields')}
|
|
4010
4333
|
className="text-primary text-sm hover:underline"
|
|
4011
4334
|
>
|
|
4012
|
-
{
|
|
4335
|
+
{t('changeOptions')}
|
|
4013
4336
|
</button>
|
|
4337
|
+
) : (
|
|
4338
|
+
!isAllDigital && (
|
|
4339
|
+
<button
|
|
4340
|
+
type="button"
|
|
4341
|
+
onClick={() => setStep(deliveryType === 'pickup' ? 'pickup' : 'shipping')}
|
|
4342
|
+
className="text-primary text-sm hover:underline"
|
|
4343
|
+
>
|
|
4344
|
+
{deliveryType === 'pickup' ? t('changePickup') : t('changeShipping')}
|
|
4345
|
+
</button>
|
|
4346
|
+
)
|
|
4014
4347
|
)}
|
|
4015
4348
|
</div>
|
|
4016
4349
|
|
|
@@ -4197,6 +4530,20 @@ function CheckoutContent() {
|
|
|
4197
4530
|
taxBreakdown={checkout.taxBreakdown}
|
|
4198
4531
|
/>
|
|
4199
4532
|
|
|
4533
|
+
{/* Custom field surcharges (one line per applied surcharge) */}
|
|
4534
|
+
{checkout.appliedSurcharges && checkout.appliedSurcharges.length > 0 && (
|
|
4535
|
+
<>
|
|
4536
|
+
{checkout.appliedSurcharges.map((s) => (
|
|
4537
|
+
<div key={s.key} className="flex items-center justify-between">
|
|
4538
|
+
<span className="text-muted-foreground">{s.name}</span>
|
|
4539
|
+
<span className="text-foreground">
|
|
4540
|
+
{formatPrice(Number(s.amount), { currency }) as string}
|
|
4541
|
+
</span>
|
|
4542
|
+
</div>
|
|
4543
|
+
))}
|
|
4544
|
+
</>
|
|
4545
|
+
)}
|
|
4546
|
+
|
|
4200
4547
|
<div className="border-border mt-2 border-t pt-2">
|
|
4201
4548
|
<div className="flex items-center justify-between">
|
|
4202
4549
|
<span className="text-foreground font-semibold">{tc('total')}</span>
|
|
@@ -11252,8 +11599,11 @@ async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production)
|
|
|
11252
11599
|
} catch {
|
|
11253
11600
|
throw new Error(`Invalid response from ${baseUrl}`);
|
|
11254
11601
|
}
|
|
11602
|
+
const storeName = json.storeName || json.name || "My Store";
|
|
11603
|
+
const displayName = json.channelName || storeName;
|
|
11255
11604
|
return {
|
|
11256
|
-
name:
|
|
11605
|
+
name: displayName,
|
|
11606
|
+
storeName,
|
|
11257
11607
|
currency: json.currency || "USD",
|
|
11258
11608
|
language: json.language || "en",
|
|
11259
11609
|
...json.i18n ? { i18n: json.i18n } : {}
|
|
@@ -11294,7 +11644,7 @@ function getCandidateApiUrls() {
|
|
|
11294
11644
|
|
|
11295
11645
|
// src/tools/get-store-info.ts
|
|
11296
11646
|
var GET_STORE_INFO_NAME = "get-store-info";
|
|
11297
|
-
var GET_STORE_INFO_DESCRIPTION = "Fetch live store information from the Brainerce API using a connection ID. Returns the store name, currency, and language. Use this to personalize the store being built.";
|
|
11647
|
+
var GET_STORE_INFO_DESCRIPTION = "Fetch live store information from the Brainerce API using a connection ID. Returns the channel display name (what the user sees in their dashboard), parent store name, currency, and language. Use this to personalize the store being built \u2014 prefer the channel name for user-facing text since a single Brainerce store can have multiple sales channels.";
|
|
11298
11648
|
var GET_STORE_INFO_SCHEMA = {
|
|
11299
11649
|
connectionId: import_zod5.z.string().describe("Vibe-coded connection ID (starts with vc_)")
|
|
11300
11650
|
};
|
|
@@ -11307,7 +11657,10 @@ async function handleGetStoreInfo(args) {
|
|
|
11307
11657
|
type: "text",
|
|
11308
11658
|
text: JSON.stringify(
|
|
11309
11659
|
{
|
|
11660
|
+
// `name` is the channel display name (from the dashboard sales-channel card);
|
|
11661
|
+
// falls back to storeName when a channel doesn't have its own name.
|
|
11310
11662
|
name: resolved.info.name,
|
|
11663
|
+
storeName: resolved.info.storeName,
|
|
11311
11664
|
currency: resolved.info.currency,
|
|
11312
11665
|
language: resolved.info.language,
|
|
11313
11666
|
connectionId: args.connectionId,
|
|
@@ -11401,7 +11754,8 @@ var GET_STORE_CAPABILITIES_SCHEMA = {
|
|
|
11401
11754
|
};
|
|
11402
11755
|
function formatCapabilities(caps) {
|
|
11403
11756
|
const lines = [];
|
|
11404
|
-
|
|
11757
|
+
const channelSuffix = caps.store.channelName && caps.store.channelName !== caps.store.name ? ` \u2014 Channel: "${caps.store.channelName}"` : "";
|
|
11758
|
+
lines.push(`## Store: ${caps.store.name} (${caps.store.currency})${channelSuffix}`);
|
|
11405
11759
|
lines.push(`Language: ${caps.store.language}`);
|
|
11406
11760
|
if (caps.store.i18n?.enabled) {
|
|
11407
11761
|
lines.push(
|
|
@@ -11434,6 +11788,9 @@ function formatCapabilities(caps) {
|
|
|
11434
11788
|
caps.features.hasDownloadableProducts ? "\u2705 Downloadable products available" : "\u274C No downloadable products"
|
|
11435
11789
|
);
|
|
11436
11790
|
lines.push(caps.features.hasCoupons ? "\u2705 Coupons available" : "\u274C No coupons");
|
|
11791
|
+
lines.push(
|
|
11792
|
+
caps.features.hasCheckoutCustomFields ? "\u2705 Checkout custom fields configured (surcharges may apply)" : "\u274C No checkout custom fields"
|
|
11793
|
+
);
|
|
11437
11794
|
lines.push("");
|
|
11438
11795
|
lines.push("## Connection Settings");
|
|
11439
11796
|
lines.push(
|
|
@@ -11479,6 +11836,11 @@ function formatCapabilities(caps) {
|
|
|
11479
11836
|
if (caps.features.hasCoupons) {
|
|
11480
11837
|
suggestions.push('Add a coupon input to the cart. Use get-code-example("coupon-input").');
|
|
11481
11838
|
}
|
|
11839
|
+
if (caps.features.hasCheckoutCustomFields) {
|
|
11840
|
+
suggestions.push(
|
|
11841
|
+
`Checkout custom fields are configured. Build a step between shipping/pickup and payment using getCheckoutCustomFields() + setCheckoutCustomFields(). Render checkout.appliedSurcharges in the order summary so customers see what they're paying for. Use get-code-example("checkout-custom-fields").`
|
|
11842
|
+
);
|
|
11843
|
+
}
|
|
11482
11844
|
if (caps.connection.requireEmailVerification) {
|
|
11483
11845
|
suggestions.push(
|
|
11484
11846
|
'Email verification is required \u2014 ALWAYS build the verify-email page. Use get-code-example("verify-email").'
|