@brainerce/mcp-server 2.7.0 → 2.9.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/index.js
CHANGED
|
@@ -438,6 +438,234 @@ function PayPalPaymentForm({ clientId, orderId, checkoutId }: { clientId: string
|
|
|
438
438
|
}
|
|
439
439
|
\`\`\``;
|
|
440
440
|
}
|
|
441
|
+
function getCheckoutCustomFieldsSection() {
|
|
442
|
+
return `## Checkout Custom Fields (Optional Step Between Shipping & Payment)
|
|
443
|
+
|
|
444
|
+
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.
|
|
445
|
+
|
|
446
|
+
### When to Build This
|
|
447
|
+
|
|
448
|
+
Check capabilities first:
|
|
449
|
+
\`\`\`typescript
|
|
450
|
+
const caps = await fetch(\`/api/vc/\${connectionId}/capabilities\`).then(r => r.json());
|
|
451
|
+
if (caps.features.hasCheckoutCustomFields) {
|
|
452
|
+
// Build the step described below
|
|
453
|
+
}
|
|
454
|
+
\`\`\`
|
|
455
|
+
|
|
456
|
+
If \`hasCheckoutCustomFields\` is true, you MUST build this step or customers will be charged surcharges they never saw.
|
|
457
|
+
|
|
458
|
+
### Where to Place It
|
|
459
|
+
|
|
460
|
+
After \`selectShippingMethod()\` (or pickup confirmation), BEFORE \`createPaymentIntent()\`. Pseudocode:
|
|
461
|
+
|
|
462
|
+
\`\`\`
|
|
463
|
+
address \u2192 shipping/pickup \u2192 custom-fields (NEW) \u2192 payment
|
|
464
|
+
\`\`\`
|
|
465
|
+
|
|
466
|
+
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.
|
|
467
|
+
|
|
468
|
+
### Loading Field Definitions
|
|
469
|
+
|
|
470
|
+
\`\`\`typescript
|
|
471
|
+
import type { CheckoutCustomFieldDefinition } from 'brainerce';
|
|
472
|
+
|
|
473
|
+
const fields = await client.getCheckoutCustomFields(checkoutId);
|
|
474
|
+
if (fields.length === 0) {
|
|
475
|
+
setStep('payment'); // Nothing to collect
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
\`\`\`
|
|
479
|
+
|
|
480
|
+
Each \`CheckoutCustomFieldDefinition\` has:
|
|
481
|
+
- \`key\` \u2014 the form field name to use in your state
|
|
482
|
+
- \`name\` \u2014 display label
|
|
483
|
+
- \`description\` \u2014 helper text (render below the input)
|
|
484
|
+
- \`type\` \u2014 \`'TEXT' | 'TEXTAREA' | 'NUMBER' | 'BOOLEAN' | 'SELECT' | 'DATE'\`
|
|
485
|
+
- \`required\` \u2014 boolean
|
|
486
|
+
- \`minLength\` / \`maxLength\` \u2014 for TEXT/TEXTAREA
|
|
487
|
+
- \`minValue\` / \`maxValue\` \u2014 for NUMBER
|
|
488
|
+
- \`options\` \u2014 \`Array<{value, label}>\` for SELECT
|
|
489
|
+
- \`pricing\` \u2014 never render this; the server uses it to compute surcharges
|
|
490
|
+
|
|
491
|
+
### Rendering All 6 Field Types
|
|
492
|
+
|
|
493
|
+
\`\`\`typescript
|
|
494
|
+
function CustomFieldsStep({ fields, values, onChange, onApply, loading }: {
|
|
495
|
+
fields: CheckoutCustomFieldDefinition[];
|
|
496
|
+
values: Record<string, unknown>;
|
|
497
|
+
onChange: (key: string, value: unknown) => void;
|
|
498
|
+
onApply: () => void;
|
|
499
|
+
loading?: boolean;
|
|
500
|
+
}) {
|
|
501
|
+
return (
|
|
502
|
+
<div className="space-y-4">
|
|
503
|
+
{fields.map((field) => {
|
|
504
|
+
const value = values[field.key] ?? '';
|
|
505
|
+
const label = (
|
|
506
|
+
<label className="block text-sm font-medium">
|
|
507
|
+
{field.name}{field.required && <span className="text-red-500"> *</span>}
|
|
508
|
+
</label>
|
|
509
|
+
);
|
|
510
|
+
const help = field.description && (
|
|
511
|
+
<p className="text-xs text-gray-500 mt-1">{field.description}</p>
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
switch (field.type) {
|
|
515
|
+
case 'TEXT':
|
|
516
|
+
return (
|
|
517
|
+
<div key={field.key}>
|
|
518
|
+
{label}
|
|
519
|
+
<input
|
|
520
|
+
type="text"
|
|
521
|
+
value={value as string}
|
|
522
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
523
|
+
required={field.required}
|
|
524
|
+
minLength={field.minLength ?? undefined}
|
|
525
|
+
maxLength={field.maxLength ?? undefined}
|
|
526
|
+
className="w-full border rounded px-3 py-2"
|
|
527
|
+
/>
|
|
528
|
+
{help}
|
|
529
|
+
</div>
|
|
530
|
+
);
|
|
531
|
+
case 'TEXTAREA':
|
|
532
|
+
return (
|
|
533
|
+
<div key={field.key}>
|
|
534
|
+
{label}
|
|
535
|
+
<textarea
|
|
536
|
+
value={value as string}
|
|
537
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
538
|
+
required={field.required}
|
|
539
|
+
minLength={field.minLength ?? undefined}
|
|
540
|
+
maxLength={field.maxLength ?? undefined}
|
|
541
|
+
rows={3}
|
|
542
|
+
className="w-full border rounded px-3 py-2"
|
|
543
|
+
/>
|
|
544
|
+
{help}
|
|
545
|
+
</div>
|
|
546
|
+
);
|
|
547
|
+
case 'NUMBER':
|
|
548
|
+
return (
|
|
549
|
+
<div key={field.key}>
|
|
550
|
+
{label}
|
|
551
|
+
<input
|
|
552
|
+
type="number"
|
|
553
|
+
value={value as number}
|
|
554
|
+
onChange={(e) => onChange(field.key, e.target.valueAsNumber)}
|
|
555
|
+
required={field.required}
|
|
556
|
+
min={field.minValue ?? undefined}
|
|
557
|
+
max={field.maxValue ?? undefined}
|
|
558
|
+
className="w-full border rounded px-3 py-2"
|
|
559
|
+
/>
|
|
560
|
+
{help}
|
|
561
|
+
</div>
|
|
562
|
+
);
|
|
563
|
+
case 'BOOLEAN':
|
|
564
|
+
return (
|
|
565
|
+
<div key={field.key} className="flex items-start gap-2">
|
|
566
|
+
<input
|
|
567
|
+
type="checkbox"
|
|
568
|
+
checked={value === true}
|
|
569
|
+
onChange={(e) => onChange(field.key, e.target.checked)}
|
|
570
|
+
className="mt-1"
|
|
571
|
+
/>
|
|
572
|
+
<div>{label}{help}</div>
|
|
573
|
+
</div>
|
|
574
|
+
);
|
|
575
|
+
case 'SELECT':
|
|
576
|
+
return (
|
|
577
|
+
<div key={field.key}>
|
|
578
|
+
{label}
|
|
579
|
+
<select
|
|
580
|
+
value={value as string}
|
|
581
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
582
|
+
required={field.required}
|
|
583
|
+
className="w-full border rounded px-3 py-2"
|
|
584
|
+
>
|
|
585
|
+
<option value="">\u2014 Select \u2014</option>
|
|
586
|
+
{field.options?.map((opt) => (
|
|
587
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
588
|
+
))}
|
|
589
|
+
</select>
|
|
590
|
+
{help}
|
|
591
|
+
</div>
|
|
592
|
+
);
|
|
593
|
+
case 'DATE':
|
|
594
|
+
return (
|
|
595
|
+
<div key={field.key}>
|
|
596
|
+
{label}
|
|
597
|
+
<input
|
|
598
|
+
type="date"
|
|
599
|
+
value={value as string}
|
|
600
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
601
|
+
required={field.required}
|
|
602
|
+
className="w-full border rounded px-3 py-2"
|
|
603
|
+
/>
|
|
604
|
+
{help}
|
|
605
|
+
</div>
|
|
606
|
+
);
|
|
607
|
+
default:
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
})}
|
|
611
|
+
|
|
612
|
+
<button
|
|
613
|
+
type="button"
|
|
614
|
+
onClick={onApply}
|
|
615
|
+
disabled={loading}
|
|
616
|
+
className="w-full bg-black text-white py-3 rounded"
|
|
617
|
+
>
|
|
618
|
+
{loading ? 'Updating\u2026' : 'Continue to Payment'}
|
|
619
|
+
</button>
|
|
620
|
+
</div>
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
\`\`\`
|
|
624
|
+
|
|
625
|
+
### Submitting Values & Recalculating Surcharges
|
|
626
|
+
|
|
627
|
+
\`\`\`typescript
|
|
628
|
+
async function applyCustomFields() {
|
|
629
|
+
setLoading(true);
|
|
630
|
+
try {
|
|
631
|
+
// Server validates required + min/max + coerces types + recalculates totals
|
|
632
|
+
const updated = await client.setCheckoutCustomFields(checkoutId, customFieldValues);
|
|
633
|
+
setCheckout(updated); // updated.surchargeAmount and updated.appliedSurcharges are now fresh
|
|
634
|
+
setStep('payment');
|
|
635
|
+
} catch (err) {
|
|
636
|
+
setError(err instanceof Error ? err.message : 'Failed to save selections');
|
|
637
|
+
} finally {
|
|
638
|
+
setLoading(false);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
\`\`\`
|
|
642
|
+
|
|
643
|
+
### Order Summary \u2014 Show the Surcharges
|
|
644
|
+
|
|
645
|
+
**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:
|
|
646
|
+
|
|
647
|
+
\`\`\`typescript
|
|
648
|
+
{checkout.appliedSurcharges?.length > 0 && (
|
|
649
|
+
<>
|
|
650
|
+
{checkout.appliedSurcharges.map((s) => (
|
|
651
|
+
<div key={s.key} className="flex justify-between text-sm text-gray-600">
|
|
652
|
+
<span>{s.name}</span>
|
|
653
|
+
<span>{formatPrice(s.amount, { currency })}</span>
|
|
654
|
+
</div>
|
|
655
|
+
))}
|
|
656
|
+
</>
|
|
657
|
+
)}
|
|
658
|
+
\`\`\`
|
|
659
|
+
|
|
660
|
+
\`checkout.surchargeAmount\` is a string (use \`parseFloat()\`); \`checkout.totalAmount\` already includes it.
|
|
661
|
+
|
|
662
|
+
### Notes
|
|
663
|
+
|
|
664
|
+
- The server filters fields by \`visibility\` (\`always\` / \`shipping\` / \`pickup\` / \`products\`) \u2014 your client just renders what \`getCheckoutCustomFields()\` returns.
|
|
665
|
+
- 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.
|
|
666
|
+
- If \`getCheckoutCustomFields()\` returns 0 fields, skip the step entirely. Don't render an empty UI.
|
|
667
|
+
`;
|
|
668
|
+
}
|
|
441
669
|
function getPaymentProvidersSection() {
|
|
442
670
|
return `## Payment Provider Detection (DO THIS FIRST on checkout page!)
|
|
443
671
|
|
|
@@ -1287,6 +1515,10 @@ ${getCheckoutFlowSection(currency)}
|
|
|
1287
1515
|
|
|
1288
1516
|
---
|
|
1289
1517
|
|
|
1518
|
+
${getCheckoutCustomFieldsSection()}
|
|
1519
|
+
|
|
1520
|
+
---
|
|
1521
|
+
|
|
1290
1522
|
${getPaymentProvidersSection()}
|
|
1291
1523
|
|
|
1292
1524
|
---
|
|
@@ -1345,6 +1577,8 @@ function getSectionByTopic(topic, connectionId, currency) {
|
|
|
1345
1577
|
return getCartSection(cur);
|
|
1346
1578
|
case "checkout":
|
|
1347
1579
|
return getCheckoutFlowSection(cur);
|
|
1580
|
+
case "checkout-custom-fields":
|
|
1581
|
+
return getCheckoutCustomFieldsSection();
|
|
1348
1582
|
case "payment":
|
|
1349
1583
|
return getPaymentProvidersSection();
|
|
1350
1584
|
case "auth":
|
|
@@ -1373,7 +1607,7 @@ function getSectionByTopic(topic, connectionId, currency) {
|
|
|
1373
1607
|
storeType: "E-commerce store"
|
|
1374
1608
|
});
|
|
1375
1609
|
default:
|
|
1376
|
-
return `Unknown topic: "${topic}". Available topics: setup, products, cart, checkout, payment, auth, order-confirmation, inventory, discounts, recommendations, tax, critical-rules, type-reference, admin, all`;
|
|
1610
|
+
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`;
|
|
1377
1611
|
}
|
|
1378
1612
|
}
|
|
1379
1613
|
|
|
@@ -1386,6 +1620,7 @@ var GET_SDK_DOCS_SCHEMA = {
|
|
|
1386
1620
|
"products",
|
|
1387
1621
|
"cart",
|
|
1388
1622
|
"checkout",
|
|
1623
|
+
"checkout-custom-fields",
|
|
1389
1624
|
"payment",
|
|
1390
1625
|
"auth",
|
|
1391
1626
|
"order-confirmation",
|
|
@@ -3407,6 +3642,7 @@ import type {
|
|
|
3407
3642
|
ShippingDestinations,
|
|
3408
3643
|
PickupLocation,
|
|
3409
3644
|
CheckoutBumpsResponse,
|
|
3645
|
+
CheckoutCustomFieldDefinition,
|
|
3410
3646
|
} from 'brainerce';
|
|
3411
3647
|
import { formatPrice } from 'brainerce';
|
|
3412
3648
|
import { getClient } from '@/lib/brainerce';
|
|
@@ -3416,6 +3652,7 @@ import { ShippingStep } from '@/components/checkout/shipping-step';
|
|
|
3416
3652
|
import { PaymentStep } from '@/components/checkout/payment-step';
|
|
3417
3653
|
import { DeliveryMethodStep } from '@/components/checkout/delivery-method-step';
|
|
3418
3654
|
import { PickupStep } from '@/components/checkout/pickup-step';
|
|
3655
|
+
import { CustomFieldsStep } from '@/components/checkout/custom-fields-step';
|
|
3419
3656
|
import { TaxDisplay } from '@/components/checkout/tax-display';
|
|
3420
3657
|
import { OrderBumpCard } from '@/components/checkout/order-bump-card';
|
|
3421
3658
|
import { CouponInput } from '@/components/cart/coupon-input';
|
|
@@ -3424,7 +3661,7 @@ import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
|
3424
3661
|
import { useTranslations } from '@/lib/translations';
|
|
3425
3662
|
import { cn } from '@/lib/utils';
|
|
3426
3663
|
|
|
3427
|
-
type CheckoutStep = 'method' | 'address' | 'shipping' | 'pickup' | 'payment';
|
|
3664
|
+
type CheckoutStep = 'method' | 'address' | 'shipping' | 'pickup' | 'custom-fields' | 'payment';
|
|
3428
3665
|
|
|
3429
3666
|
function CheckoutContent() {
|
|
3430
3667
|
const searchParams = useSearchParams();
|
|
@@ -3457,6 +3694,9 @@ function CheckoutContent() {
|
|
|
3457
3694
|
const [orderBumps, setOrderBumps] = useState<CheckoutBumpsResponse | null>(null);
|
|
3458
3695
|
const [addedBumpIds, setAddedBumpIds] = useState<Set<string>>(new Set());
|
|
3459
3696
|
const [bumpLoading, setBumpLoading] = useState<string | null>(null);
|
|
3697
|
+
const [customFields, setCustomFields] = useState<CheckoutCustomFieldDefinition[]>([]);
|
|
3698
|
+
const [customFieldValues, setCustomFieldValues] = useState<Record<string, unknown>>({});
|
|
3699
|
+
const [customFieldsLoading, setCustomFieldsLoading] = useState(false);
|
|
3460
3700
|
|
|
3461
3701
|
// Check for returning from canceled payment
|
|
3462
3702
|
const canceled = searchParams.get('canceled') === 'true';
|
|
@@ -3508,6 +3748,23 @@ function CheckoutContent() {
|
|
|
3508
3748
|
const existing = await client.getCheckout(existingCheckoutId);
|
|
3509
3749
|
setCheckout(existing);
|
|
3510
3750
|
|
|
3751
|
+
// Preload custom field definitions and any existing values so the
|
|
3752
|
+
// step indicator and "change options" affordance work on resume.
|
|
3753
|
+
client
|
|
3754
|
+
.getCheckoutCustomFields(existing.id)
|
|
3755
|
+
.then((fields) => {
|
|
3756
|
+
setCustomFields(fields);
|
|
3757
|
+
const existingValues = (
|
|
3758
|
+
existing as unknown as {
|
|
3759
|
+
customFieldValues?: Record<string, unknown> | null;
|
|
3760
|
+
}
|
|
3761
|
+
).customFieldValues;
|
|
3762
|
+
if (existingValues) setCustomFieldValues(existingValues);
|
|
3763
|
+
})
|
|
3764
|
+
.catch(() => {
|
|
3765
|
+
setCustomFields([]);
|
|
3766
|
+
});
|
|
3767
|
+
|
|
3511
3768
|
// Determine step based on checkout state
|
|
3512
3769
|
const allDigital = existing.lineItems.every(
|
|
3513
3770
|
(i) => (i.product as unknown as { isDownloadable?: boolean }).isDownloadable
|
|
@@ -3678,6 +3935,21 @@ function CheckoutContent() {
|
|
|
3678
3935
|
}
|
|
3679
3936
|
}
|
|
3680
3937
|
|
|
3938
|
+
// After shipping/pickup is set, decide whether to show the custom-fields step
|
|
3939
|
+
// or jump straight to payment. Returns the next step.
|
|
3940
|
+
async function loadCustomFieldsOrSkip(checkoutId: string): Promise<CheckoutStep> {
|
|
3941
|
+
try {
|
|
3942
|
+
const fields = await getClient().getCheckoutCustomFields(checkoutId);
|
|
3943
|
+
setCustomFields(fields);
|
|
3944
|
+
return fields.length > 0 ? 'custom-fields' : 'payment';
|
|
3945
|
+
} catch {
|
|
3946
|
+
// If the endpoint isn't available or fails, fall through to payment
|
|
3947
|
+
// rather than blocking the customer.
|
|
3948
|
+
setCustomFields([]);
|
|
3949
|
+
return 'payment';
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3681
3953
|
// Handle shipping method selection
|
|
3682
3954
|
async function handleShippingSelect(rateId: string) {
|
|
3683
3955
|
if (!checkout) return;
|
|
@@ -3690,7 +3962,7 @@ function CheckoutContent() {
|
|
|
3690
3962
|
|
|
3691
3963
|
const updated = await client.selectShippingMethod(checkout.id, rateId);
|
|
3692
3964
|
setCheckout(updated);
|
|
3693
|
-
setStep(
|
|
3965
|
+
setStep(await loadCustomFieldsOrSkip(updated.id));
|
|
3694
3966
|
} catch (err) {
|
|
3695
3967
|
const message = err instanceof Error ? err.message : t('failedToSelectShipping');
|
|
3696
3968
|
setError(message);
|
|
@@ -3699,6 +3971,23 @@ function CheckoutContent() {
|
|
|
3699
3971
|
}
|
|
3700
3972
|
}
|
|
3701
3973
|
|
|
3974
|
+
// Submit custom fields
|
|
3975
|
+
async function handleCustomFieldsApply() {
|
|
3976
|
+
if (!checkout) return;
|
|
3977
|
+
try {
|
|
3978
|
+
setCustomFieldsLoading(true);
|
|
3979
|
+
setError(null);
|
|
3980
|
+
const updated = await getClient().setCheckoutCustomFields(checkout.id, customFieldValues);
|
|
3981
|
+
setCheckout(updated);
|
|
3982
|
+
setStep('payment');
|
|
3983
|
+
} catch (err) {
|
|
3984
|
+
const message = err instanceof Error ? err.message : t('customFieldsFailed');
|
|
3985
|
+
setError(message);
|
|
3986
|
+
} finally {
|
|
3987
|
+
setCustomFieldsLoading(false);
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
|
|
3702
3991
|
// Handle delivery method selection
|
|
3703
3992
|
async function handleDeliveryTypeSelect(method: 'shipping' | 'pickup') {
|
|
3704
3993
|
if (!checkout) return;
|
|
@@ -3744,7 +4033,7 @@ function CheckoutContent() {
|
|
|
3744
4033
|
phone: customerInfo.phone,
|
|
3745
4034
|
});
|
|
3746
4035
|
setCheckout(updated);
|
|
3747
|
-
setStep(
|
|
4036
|
+
setStep(await loadCustomFieldsOrSkip(updated.id));
|
|
3748
4037
|
} catch (err) {
|
|
3749
4038
|
const message = err instanceof Error ? err.message : t('failedToSelectPickup');
|
|
3750
4039
|
setError(message);
|
|
@@ -3806,9 +4095,15 @@ function CheckoutContent() {
|
|
|
3806
4095
|
);
|
|
3807
4096
|
}
|
|
3808
4097
|
|
|
4098
|
+
const customFieldsStep =
|
|
4099
|
+
customFields.length > 0
|
|
4100
|
+
? [{ key: 'custom-fields' as CheckoutStep, label: t('stepCustomFields') }]
|
|
4101
|
+
: [];
|
|
4102
|
+
|
|
3809
4103
|
const steps: { key: CheckoutStep; label: string }[] = isAllDigital
|
|
3810
4104
|
? [
|
|
3811
4105
|
{ key: 'address', label: t('stepContactInfo') },
|
|
4106
|
+
...customFieldsStep,
|
|
3812
4107
|
{ key: 'payment', label: t('stepPayment') },
|
|
3813
4108
|
]
|
|
3814
4109
|
: pickupLocations.length > 0
|
|
@@ -3816,17 +4111,20 @@ function CheckoutContent() {
|
|
|
3816
4111
|
? [
|
|
3817
4112
|
{ key: 'method', label: t('stepMethod') },
|
|
3818
4113
|
{ key: 'pickup', label: t('stepPickup') },
|
|
4114
|
+
...customFieldsStep,
|
|
3819
4115
|
{ key: 'payment', label: t('stepPayment') },
|
|
3820
4116
|
]
|
|
3821
4117
|
: [
|
|
3822
4118
|
{ key: 'method', label: t('stepMethod') },
|
|
3823
4119
|
{ key: 'address', label: t('stepAddress') },
|
|
3824
4120
|
{ key: 'shipping', label: t('stepShipping') },
|
|
4121
|
+
...customFieldsStep,
|
|
3825
4122
|
{ key: 'payment', label: t('stepPayment') },
|
|
3826
4123
|
]
|
|
3827
4124
|
: [
|
|
3828
4125
|
{ key: 'address', label: t('stepAddress') },
|
|
3829
4126
|
{ key: 'shipping', label: t('stepShipping') },
|
|
4127
|
+
...customFieldsStep,
|
|
3830
4128
|
{ key: 'payment', label: t('stepPayment') },
|
|
3831
4129
|
];
|
|
3832
4130
|
|
|
@@ -4025,19 +4323,54 @@ function CheckoutContent() {
|
|
|
4025
4323
|
</div>
|
|
4026
4324
|
)}
|
|
4027
4325
|
|
|
4326
|
+
{/* Custom Fields (optional, between shipping/pickup and payment) */}
|
|
4327
|
+
{step === 'custom-fields' && checkout && (
|
|
4328
|
+
<div>
|
|
4329
|
+
<div className="mb-4 flex items-center justify-between">
|
|
4330
|
+
<h2 className="text-foreground text-lg font-semibold">{t('customFieldsTitle')}</h2>
|
|
4331
|
+
<button
|
|
4332
|
+
type="button"
|
|
4333
|
+
onClick={() => setStep(deliveryType === 'pickup' ? 'pickup' : 'shipping')}
|
|
4334
|
+
className="text-primary text-sm hover:underline"
|
|
4335
|
+
>
|
|
4336
|
+
{deliveryType === 'pickup' ? t('changePickup') : t('changeShipping')}
|
|
4337
|
+
</button>
|
|
4338
|
+
</div>
|
|
4339
|
+
<CustomFieldsStep
|
|
4340
|
+
fields={customFields}
|
|
4341
|
+
values={customFieldValues}
|
|
4342
|
+
onChange={(key, value) =>
|
|
4343
|
+
setCustomFieldValues((prev) => ({ ...prev, [key]: value }))
|
|
4344
|
+
}
|
|
4345
|
+
onApply={handleCustomFieldsApply}
|
|
4346
|
+
loading={customFieldsLoading}
|
|
4347
|
+
/>
|
|
4348
|
+
</div>
|
|
4349
|
+
)}
|
|
4350
|
+
|
|
4028
4351
|
{/* Payment */}
|
|
4029
4352
|
{step === 'payment' && checkout && (
|
|
4030
4353
|
<div>
|
|
4031
4354
|
<div className="mb-4 flex items-center justify-between">
|
|
4032
4355
|
<h2 className="text-foreground text-lg font-semibold">{t('payment')}</h2>
|
|
4033
|
-
{
|
|
4356
|
+
{customFields.length > 0 ? (
|
|
4034
4357
|
<button
|
|
4035
4358
|
type="button"
|
|
4036
|
-
onClick={() => setStep(
|
|
4359
|
+
onClick={() => setStep('custom-fields')}
|
|
4037
4360
|
className="text-primary text-sm hover:underline"
|
|
4038
4361
|
>
|
|
4039
|
-
{
|
|
4362
|
+
{t('changeOptions')}
|
|
4040
4363
|
</button>
|
|
4364
|
+
) : (
|
|
4365
|
+
!isAllDigital && (
|
|
4366
|
+
<button
|
|
4367
|
+
type="button"
|
|
4368
|
+
onClick={() => setStep(deliveryType === 'pickup' ? 'pickup' : 'shipping')}
|
|
4369
|
+
className="text-primary text-sm hover:underline"
|
|
4370
|
+
>
|
|
4371
|
+
{deliveryType === 'pickup' ? t('changePickup') : t('changeShipping')}
|
|
4372
|
+
</button>
|
|
4373
|
+
)
|
|
4041
4374
|
)}
|
|
4042
4375
|
</div>
|
|
4043
4376
|
|
|
@@ -4224,6 +4557,20 @@ function CheckoutContent() {
|
|
|
4224
4557
|
taxBreakdown={checkout.taxBreakdown}
|
|
4225
4558
|
/>
|
|
4226
4559
|
|
|
4560
|
+
{/* Custom field surcharges (one line per applied surcharge) */}
|
|
4561
|
+
{checkout.appliedSurcharges && checkout.appliedSurcharges.length > 0 && (
|
|
4562
|
+
<>
|
|
4563
|
+
{checkout.appliedSurcharges.map((s) => (
|
|
4564
|
+
<div key={s.key} className="flex items-center justify-between">
|
|
4565
|
+
<span className="text-muted-foreground">{s.name}</span>
|
|
4566
|
+
<span className="text-foreground">
|
|
4567
|
+
{formatPrice(Number(s.amount), { currency }) as string}
|
|
4568
|
+
</span>
|
|
4569
|
+
</div>
|
|
4570
|
+
))}
|
|
4571
|
+
</>
|
|
4572
|
+
)}
|
|
4573
|
+
|
|
4227
4574
|
<div className="border-border mt-2 border-t pt-2">
|
|
4228
4575
|
<div className="flex items-center justify-between">
|
|
4229
4576
|
<span className="text-foreground font-semibold">{tc('total')}</span>
|
|
@@ -11279,8 +11626,11 @@ async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production)
|
|
|
11279
11626
|
} catch {
|
|
11280
11627
|
throw new Error(`Invalid response from ${baseUrl}`);
|
|
11281
11628
|
}
|
|
11629
|
+
const storeName = json.storeName || json.name || "My Store";
|
|
11630
|
+
const displayName = json.channelName || storeName;
|
|
11282
11631
|
return {
|
|
11283
|
-
name:
|
|
11632
|
+
name: displayName,
|
|
11633
|
+
storeName,
|
|
11284
11634
|
currency: json.currency || "USD",
|
|
11285
11635
|
language: json.language || "en",
|
|
11286
11636
|
...json.i18n ? { i18n: json.i18n } : {}
|
|
@@ -11321,7 +11671,7 @@ function getCandidateApiUrls() {
|
|
|
11321
11671
|
|
|
11322
11672
|
// src/tools/get-store-info.ts
|
|
11323
11673
|
var GET_STORE_INFO_NAME = "get-store-info";
|
|
11324
|
-
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.";
|
|
11674
|
+
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.";
|
|
11325
11675
|
var GET_STORE_INFO_SCHEMA = {
|
|
11326
11676
|
connectionId: import_zod5.z.string().describe("Vibe-coded connection ID (starts with vc_)")
|
|
11327
11677
|
};
|
|
@@ -11334,7 +11684,10 @@ async function handleGetStoreInfo(args) {
|
|
|
11334
11684
|
type: "text",
|
|
11335
11685
|
text: JSON.stringify(
|
|
11336
11686
|
{
|
|
11687
|
+
// `name` is the channel display name (from the dashboard sales-channel card);
|
|
11688
|
+
// falls back to storeName when a channel doesn't have its own name.
|
|
11337
11689
|
name: resolved.info.name,
|
|
11690
|
+
storeName: resolved.info.storeName,
|
|
11338
11691
|
currency: resolved.info.currency,
|
|
11339
11692
|
language: resolved.info.language,
|
|
11340
11693
|
connectionId: args.connectionId,
|
|
@@ -11428,7 +11781,8 @@ var GET_STORE_CAPABILITIES_SCHEMA = {
|
|
|
11428
11781
|
};
|
|
11429
11782
|
function formatCapabilities(caps) {
|
|
11430
11783
|
const lines = [];
|
|
11431
|
-
|
|
11784
|
+
const channelSuffix = caps.store.channelName && caps.store.channelName !== caps.store.name ? ` \u2014 Channel: "${caps.store.channelName}"` : "";
|
|
11785
|
+
lines.push(`## Store: ${caps.store.name} (${caps.store.currency})${channelSuffix}`);
|
|
11432
11786
|
lines.push(`Language: ${caps.store.language}`);
|
|
11433
11787
|
if (caps.store.i18n?.enabled) {
|
|
11434
11788
|
lines.push(
|
|
@@ -11461,6 +11815,9 @@ function formatCapabilities(caps) {
|
|
|
11461
11815
|
caps.features.hasDownloadableProducts ? "\u2705 Downloadable products available" : "\u274C No downloadable products"
|
|
11462
11816
|
);
|
|
11463
11817
|
lines.push(caps.features.hasCoupons ? "\u2705 Coupons available" : "\u274C No coupons");
|
|
11818
|
+
lines.push(
|
|
11819
|
+
caps.features.hasCheckoutCustomFields ? "\u2705 Checkout custom fields configured (surcharges may apply)" : "\u274C No checkout custom fields"
|
|
11820
|
+
);
|
|
11464
11821
|
lines.push("");
|
|
11465
11822
|
lines.push("## Connection Settings");
|
|
11466
11823
|
lines.push(
|
|
@@ -11506,6 +11863,11 @@ function formatCapabilities(caps) {
|
|
|
11506
11863
|
if (caps.features.hasCoupons) {
|
|
11507
11864
|
suggestions.push('Add a coupon input to the cart. Use get-code-example("coupon-input").');
|
|
11508
11865
|
}
|
|
11866
|
+
if (caps.features.hasCheckoutCustomFields) {
|
|
11867
|
+
suggestions.push(
|
|
11868
|
+
`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").`
|
|
11869
|
+
);
|
|
11870
|
+
}
|
|
11509
11871
|
if (caps.connection.requireEmailVerification) {
|
|
11510
11872
|
suggestions.push(
|
|
11511
11873
|
'Email verification is required \u2014 ALWAYS build the verify-email page. Use get-code-example("verify-email").'
|