@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.mjs
CHANGED
|
@@ -403,6 +403,234 @@ function PayPalPaymentForm({ clientId, orderId, checkoutId }: { clientId: string
|
|
|
403
403
|
}
|
|
404
404
|
\`\`\``;
|
|
405
405
|
}
|
|
406
|
+
function getCheckoutCustomFieldsSection() {
|
|
407
|
+
return `## Checkout Custom Fields (Optional Step Between Shipping & Payment)
|
|
408
|
+
|
|
409
|
+
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.
|
|
410
|
+
|
|
411
|
+
### When to Build This
|
|
412
|
+
|
|
413
|
+
Check capabilities first:
|
|
414
|
+
\`\`\`typescript
|
|
415
|
+
const caps = await fetch(\`/api/vc/\${connectionId}/capabilities\`).then(r => r.json());
|
|
416
|
+
if (caps.features.hasCheckoutCustomFields) {
|
|
417
|
+
// Build the step described below
|
|
418
|
+
}
|
|
419
|
+
\`\`\`
|
|
420
|
+
|
|
421
|
+
If \`hasCheckoutCustomFields\` is true, you MUST build this step or customers will be charged surcharges they never saw.
|
|
422
|
+
|
|
423
|
+
### Where to Place It
|
|
424
|
+
|
|
425
|
+
After \`selectShippingMethod()\` (or pickup confirmation), BEFORE \`createPaymentIntent()\`. Pseudocode:
|
|
426
|
+
|
|
427
|
+
\`\`\`
|
|
428
|
+
address \u2192 shipping/pickup \u2192 custom-fields (NEW) \u2192 payment
|
|
429
|
+
\`\`\`
|
|
430
|
+
|
|
431
|
+
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.
|
|
432
|
+
|
|
433
|
+
### Loading Field Definitions
|
|
434
|
+
|
|
435
|
+
\`\`\`typescript
|
|
436
|
+
import type { CheckoutCustomFieldDefinition } from 'brainerce';
|
|
437
|
+
|
|
438
|
+
const fields = await client.getCheckoutCustomFields(checkoutId);
|
|
439
|
+
if (fields.length === 0) {
|
|
440
|
+
setStep('payment'); // Nothing to collect
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
\`\`\`
|
|
444
|
+
|
|
445
|
+
Each \`CheckoutCustomFieldDefinition\` has:
|
|
446
|
+
- \`key\` \u2014 the form field name to use in your state
|
|
447
|
+
- \`name\` \u2014 display label
|
|
448
|
+
- \`description\` \u2014 helper text (render below the input)
|
|
449
|
+
- \`type\` \u2014 \`'TEXT' | 'TEXTAREA' | 'NUMBER' | 'BOOLEAN' | 'SELECT' | 'DATE'\`
|
|
450
|
+
- \`required\` \u2014 boolean
|
|
451
|
+
- \`minLength\` / \`maxLength\` \u2014 for TEXT/TEXTAREA
|
|
452
|
+
- \`minValue\` / \`maxValue\` \u2014 for NUMBER
|
|
453
|
+
- \`options\` \u2014 \`Array<{value, label}>\` for SELECT
|
|
454
|
+
- \`pricing\` \u2014 never render this; the server uses it to compute surcharges
|
|
455
|
+
|
|
456
|
+
### Rendering All 6 Field Types
|
|
457
|
+
|
|
458
|
+
\`\`\`typescript
|
|
459
|
+
function CustomFieldsStep({ fields, values, onChange, onApply, loading }: {
|
|
460
|
+
fields: CheckoutCustomFieldDefinition[];
|
|
461
|
+
values: Record<string, unknown>;
|
|
462
|
+
onChange: (key: string, value: unknown) => void;
|
|
463
|
+
onApply: () => void;
|
|
464
|
+
loading?: boolean;
|
|
465
|
+
}) {
|
|
466
|
+
return (
|
|
467
|
+
<div className="space-y-4">
|
|
468
|
+
{fields.map((field) => {
|
|
469
|
+
const value = values[field.key] ?? '';
|
|
470
|
+
const label = (
|
|
471
|
+
<label className="block text-sm font-medium">
|
|
472
|
+
{field.name}{field.required && <span className="text-red-500"> *</span>}
|
|
473
|
+
</label>
|
|
474
|
+
);
|
|
475
|
+
const help = field.description && (
|
|
476
|
+
<p className="text-xs text-gray-500 mt-1">{field.description}</p>
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
switch (field.type) {
|
|
480
|
+
case 'TEXT':
|
|
481
|
+
return (
|
|
482
|
+
<div key={field.key}>
|
|
483
|
+
{label}
|
|
484
|
+
<input
|
|
485
|
+
type="text"
|
|
486
|
+
value={value as string}
|
|
487
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
488
|
+
required={field.required}
|
|
489
|
+
minLength={field.minLength ?? undefined}
|
|
490
|
+
maxLength={field.maxLength ?? undefined}
|
|
491
|
+
className="w-full border rounded px-3 py-2"
|
|
492
|
+
/>
|
|
493
|
+
{help}
|
|
494
|
+
</div>
|
|
495
|
+
);
|
|
496
|
+
case 'TEXTAREA':
|
|
497
|
+
return (
|
|
498
|
+
<div key={field.key}>
|
|
499
|
+
{label}
|
|
500
|
+
<textarea
|
|
501
|
+
value={value as string}
|
|
502
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
503
|
+
required={field.required}
|
|
504
|
+
minLength={field.minLength ?? undefined}
|
|
505
|
+
maxLength={field.maxLength ?? undefined}
|
|
506
|
+
rows={3}
|
|
507
|
+
className="w-full border rounded px-3 py-2"
|
|
508
|
+
/>
|
|
509
|
+
{help}
|
|
510
|
+
</div>
|
|
511
|
+
);
|
|
512
|
+
case 'NUMBER':
|
|
513
|
+
return (
|
|
514
|
+
<div key={field.key}>
|
|
515
|
+
{label}
|
|
516
|
+
<input
|
|
517
|
+
type="number"
|
|
518
|
+
value={value as number}
|
|
519
|
+
onChange={(e) => onChange(field.key, e.target.valueAsNumber)}
|
|
520
|
+
required={field.required}
|
|
521
|
+
min={field.minValue ?? undefined}
|
|
522
|
+
max={field.maxValue ?? undefined}
|
|
523
|
+
className="w-full border rounded px-3 py-2"
|
|
524
|
+
/>
|
|
525
|
+
{help}
|
|
526
|
+
</div>
|
|
527
|
+
);
|
|
528
|
+
case 'BOOLEAN':
|
|
529
|
+
return (
|
|
530
|
+
<div key={field.key} className="flex items-start gap-2">
|
|
531
|
+
<input
|
|
532
|
+
type="checkbox"
|
|
533
|
+
checked={value === true}
|
|
534
|
+
onChange={(e) => onChange(field.key, e.target.checked)}
|
|
535
|
+
className="mt-1"
|
|
536
|
+
/>
|
|
537
|
+
<div>{label}{help}</div>
|
|
538
|
+
</div>
|
|
539
|
+
);
|
|
540
|
+
case 'SELECT':
|
|
541
|
+
return (
|
|
542
|
+
<div key={field.key}>
|
|
543
|
+
{label}
|
|
544
|
+
<select
|
|
545
|
+
value={value as string}
|
|
546
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
547
|
+
required={field.required}
|
|
548
|
+
className="w-full border rounded px-3 py-2"
|
|
549
|
+
>
|
|
550
|
+
<option value="">\u2014 Select \u2014</option>
|
|
551
|
+
{field.options?.map((opt) => (
|
|
552
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
553
|
+
))}
|
|
554
|
+
</select>
|
|
555
|
+
{help}
|
|
556
|
+
</div>
|
|
557
|
+
);
|
|
558
|
+
case 'DATE':
|
|
559
|
+
return (
|
|
560
|
+
<div key={field.key}>
|
|
561
|
+
{label}
|
|
562
|
+
<input
|
|
563
|
+
type="date"
|
|
564
|
+
value={value as string}
|
|
565
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
566
|
+
required={field.required}
|
|
567
|
+
className="w-full border rounded px-3 py-2"
|
|
568
|
+
/>
|
|
569
|
+
{help}
|
|
570
|
+
</div>
|
|
571
|
+
);
|
|
572
|
+
default:
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
})}
|
|
576
|
+
|
|
577
|
+
<button
|
|
578
|
+
type="button"
|
|
579
|
+
onClick={onApply}
|
|
580
|
+
disabled={loading}
|
|
581
|
+
className="w-full bg-black text-white py-3 rounded"
|
|
582
|
+
>
|
|
583
|
+
{loading ? 'Updating\u2026' : 'Continue to Payment'}
|
|
584
|
+
</button>
|
|
585
|
+
</div>
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
\`\`\`
|
|
589
|
+
|
|
590
|
+
### Submitting Values & Recalculating Surcharges
|
|
591
|
+
|
|
592
|
+
\`\`\`typescript
|
|
593
|
+
async function applyCustomFields() {
|
|
594
|
+
setLoading(true);
|
|
595
|
+
try {
|
|
596
|
+
// Server validates required + min/max + coerces types + recalculates totals
|
|
597
|
+
const updated = await client.setCheckoutCustomFields(checkoutId, customFieldValues);
|
|
598
|
+
setCheckout(updated); // updated.surchargeAmount and updated.appliedSurcharges are now fresh
|
|
599
|
+
setStep('payment');
|
|
600
|
+
} catch (err) {
|
|
601
|
+
setError(err instanceof Error ? err.message : 'Failed to save selections');
|
|
602
|
+
} finally {
|
|
603
|
+
setLoading(false);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
\`\`\`
|
|
607
|
+
|
|
608
|
+
### Order Summary \u2014 Show the Surcharges
|
|
609
|
+
|
|
610
|
+
**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:
|
|
611
|
+
|
|
612
|
+
\`\`\`typescript
|
|
613
|
+
{checkout.appliedSurcharges?.length > 0 && (
|
|
614
|
+
<>
|
|
615
|
+
{checkout.appliedSurcharges.map((s) => (
|
|
616
|
+
<div key={s.key} className="flex justify-between text-sm text-gray-600">
|
|
617
|
+
<span>{s.name}</span>
|
|
618
|
+
<span>{formatPrice(s.amount, { currency })}</span>
|
|
619
|
+
</div>
|
|
620
|
+
))}
|
|
621
|
+
</>
|
|
622
|
+
)}
|
|
623
|
+
\`\`\`
|
|
624
|
+
|
|
625
|
+
\`checkout.surchargeAmount\` is a string (use \`parseFloat()\`); \`checkout.totalAmount\` already includes it.
|
|
626
|
+
|
|
627
|
+
### Notes
|
|
628
|
+
|
|
629
|
+
- The server filters fields by \`visibility\` (\`always\` / \`shipping\` / \`pickup\` / \`products\`) \u2014 your client just renders what \`getCheckoutCustomFields()\` returns.
|
|
630
|
+
- 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.
|
|
631
|
+
- If \`getCheckoutCustomFields()\` returns 0 fields, skip the step entirely. Don't render an empty UI.
|
|
632
|
+
`;
|
|
633
|
+
}
|
|
406
634
|
function getPaymentProvidersSection() {
|
|
407
635
|
return `## Payment Provider Detection (DO THIS FIRST on checkout page!)
|
|
408
636
|
|
|
@@ -1252,6 +1480,10 @@ ${getCheckoutFlowSection(currency)}
|
|
|
1252
1480
|
|
|
1253
1481
|
---
|
|
1254
1482
|
|
|
1483
|
+
${getCheckoutCustomFieldsSection()}
|
|
1484
|
+
|
|
1485
|
+
---
|
|
1486
|
+
|
|
1255
1487
|
${getPaymentProvidersSection()}
|
|
1256
1488
|
|
|
1257
1489
|
---
|
|
@@ -1310,6 +1542,8 @@ function getSectionByTopic(topic, connectionId, currency) {
|
|
|
1310
1542
|
return getCartSection(cur);
|
|
1311
1543
|
case "checkout":
|
|
1312
1544
|
return getCheckoutFlowSection(cur);
|
|
1545
|
+
case "checkout-custom-fields":
|
|
1546
|
+
return getCheckoutCustomFieldsSection();
|
|
1313
1547
|
case "payment":
|
|
1314
1548
|
return getPaymentProvidersSection();
|
|
1315
1549
|
case "auth":
|
|
@@ -1338,7 +1572,7 @@ function getSectionByTopic(topic, connectionId, currency) {
|
|
|
1338
1572
|
storeType: "E-commerce store"
|
|
1339
1573
|
});
|
|
1340
1574
|
default:
|
|
1341
|
-
return `Unknown topic: "${topic}". Available topics: setup, products, cart, checkout, payment, auth, order-confirmation, inventory, discounts, recommendations, tax, critical-rules, type-reference, admin, all`;
|
|
1575
|
+
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`;
|
|
1342
1576
|
}
|
|
1343
1577
|
}
|
|
1344
1578
|
|
|
@@ -1351,6 +1585,7 @@ var GET_SDK_DOCS_SCHEMA = {
|
|
|
1351
1585
|
"products",
|
|
1352
1586
|
"cart",
|
|
1353
1587
|
"checkout",
|
|
1588
|
+
"checkout-custom-fields",
|
|
1354
1589
|
"payment",
|
|
1355
1590
|
"auth",
|
|
1356
1591
|
"order-confirmation",
|
|
@@ -3372,6 +3607,7 @@ import type {
|
|
|
3372
3607
|
ShippingDestinations,
|
|
3373
3608
|
PickupLocation,
|
|
3374
3609
|
CheckoutBumpsResponse,
|
|
3610
|
+
CheckoutCustomFieldDefinition,
|
|
3375
3611
|
} from 'brainerce';
|
|
3376
3612
|
import { formatPrice } from 'brainerce';
|
|
3377
3613
|
import { getClient } from '@/lib/brainerce';
|
|
@@ -3381,6 +3617,7 @@ import { ShippingStep } from '@/components/checkout/shipping-step';
|
|
|
3381
3617
|
import { PaymentStep } from '@/components/checkout/payment-step';
|
|
3382
3618
|
import { DeliveryMethodStep } from '@/components/checkout/delivery-method-step';
|
|
3383
3619
|
import { PickupStep } from '@/components/checkout/pickup-step';
|
|
3620
|
+
import { CustomFieldsStep } from '@/components/checkout/custom-fields-step';
|
|
3384
3621
|
import { TaxDisplay } from '@/components/checkout/tax-display';
|
|
3385
3622
|
import { OrderBumpCard } from '@/components/checkout/order-bump-card';
|
|
3386
3623
|
import { CouponInput } from '@/components/cart/coupon-input';
|
|
@@ -3389,7 +3626,7 @@ import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
|
3389
3626
|
import { useTranslations } from '@/lib/translations';
|
|
3390
3627
|
import { cn } from '@/lib/utils';
|
|
3391
3628
|
|
|
3392
|
-
type CheckoutStep = 'method' | 'address' | 'shipping' | 'pickup' | 'payment';
|
|
3629
|
+
type CheckoutStep = 'method' | 'address' | 'shipping' | 'pickup' | 'custom-fields' | 'payment';
|
|
3393
3630
|
|
|
3394
3631
|
function CheckoutContent() {
|
|
3395
3632
|
const searchParams = useSearchParams();
|
|
@@ -3422,6 +3659,9 @@ function CheckoutContent() {
|
|
|
3422
3659
|
const [orderBumps, setOrderBumps] = useState<CheckoutBumpsResponse | null>(null);
|
|
3423
3660
|
const [addedBumpIds, setAddedBumpIds] = useState<Set<string>>(new Set());
|
|
3424
3661
|
const [bumpLoading, setBumpLoading] = useState<string | null>(null);
|
|
3662
|
+
const [customFields, setCustomFields] = useState<CheckoutCustomFieldDefinition[]>([]);
|
|
3663
|
+
const [customFieldValues, setCustomFieldValues] = useState<Record<string, unknown>>({});
|
|
3664
|
+
const [customFieldsLoading, setCustomFieldsLoading] = useState(false);
|
|
3425
3665
|
|
|
3426
3666
|
// Check for returning from canceled payment
|
|
3427
3667
|
const canceled = searchParams.get('canceled') === 'true';
|
|
@@ -3473,6 +3713,23 @@ function CheckoutContent() {
|
|
|
3473
3713
|
const existing = await client.getCheckout(existingCheckoutId);
|
|
3474
3714
|
setCheckout(existing);
|
|
3475
3715
|
|
|
3716
|
+
// Preload custom field definitions and any existing values so the
|
|
3717
|
+
// step indicator and "change options" affordance work on resume.
|
|
3718
|
+
client
|
|
3719
|
+
.getCheckoutCustomFields(existing.id)
|
|
3720
|
+
.then((fields) => {
|
|
3721
|
+
setCustomFields(fields);
|
|
3722
|
+
const existingValues = (
|
|
3723
|
+
existing as unknown as {
|
|
3724
|
+
customFieldValues?: Record<string, unknown> | null;
|
|
3725
|
+
}
|
|
3726
|
+
).customFieldValues;
|
|
3727
|
+
if (existingValues) setCustomFieldValues(existingValues);
|
|
3728
|
+
})
|
|
3729
|
+
.catch(() => {
|
|
3730
|
+
setCustomFields([]);
|
|
3731
|
+
});
|
|
3732
|
+
|
|
3476
3733
|
// Determine step based on checkout state
|
|
3477
3734
|
const allDigital = existing.lineItems.every(
|
|
3478
3735
|
(i) => (i.product as unknown as { isDownloadable?: boolean }).isDownloadable
|
|
@@ -3643,6 +3900,21 @@ function CheckoutContent() {
|
|
|
3643
3900
|
}
|
|
3644
3901
|
}
|
|
3645
3902
|
|
|
3903
|
+
// After shipping/pickup is set, decide whether to show the custom-fields step
|
|
3904
|
+
// or jump straight to payment. Returns the next step.
|
|
3905
|
+
async function loadCustomFieldsOrSkip(checkoutId: string): Promise<CheckoutStep> {
|
|
3906
|
+
try {
|
|
3907
|
+
const fields = await getClient().getCheckoutCustomFields(checkoutId);
|
|
3908
|
+
setCustomFields(fields);
|
|
3909
|
+
return fields.length > 0 ? 'custom-fields' : 'payment';
|
|
3910
|
+
} catch {
|
|
3911
|
+
// If the endpoint isn't available or fails, fall through to payment
|
|
3912
|
+
// rather than blocking the customer.
|
|
3913
|
+
setCustomFields([]);
|
|
3914
|
+
return 'payment';
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3646
3918
|
// Handle shipping method selection
|
|
3647
3919
|
async function handleShippingSelect(rateId: string) {
|
|
3648
3920
|
if (!checkout) return;
|
|
@@ -3655,7 +3927,7 @@ function CheckoutContent() {
|
|
|
3655
3927
|
|
|
3656
3928
|
const updated = await client.selectShippingMethod(checkout.id, rateId);
|
|
3657
3929
|
setCheckout(updated);
|
|
3658
|
-
setStep(
|
|
3930
|
+
setStep(await loadCustomFieldsOrSkip(updated.id));
|
|
3659
3931
|
} catch (err) {
|
|
3660
3932
|
const message = err instanceof Error ? err.message : t('failedToSelectShipping');
|
|
3661
3933
|
setError(message);
|
|
@@ -3664,6 +3936,23 @@ function CheckoutContent() {
|
|
|
3664
3936
|
}
|
|
3665
3937
|
}
|
|
3666
3938
|
|
|
3939
|
+
// Submit custom fields
|
|
3940
|
+
async function handleCustomFieldsApply() {
|
|
3941
|
+
if (!checkout) return;
|
|
3942
|
+
try {
|
|
3943
|
+
setCustomFieldsLoading(true);
|
|
3944
|
+
setError(null);
|
|
3945
|
+
const updated = await getClient().setCheckoutCustomFields(checkout.id, customFieldValues);
|
|
3946
|
+
setCheckout(updated);
|
|
3947
|
+
setStep('payment');
|
|
3948
|
+
} catch (err) {
|
|
3949
|
+
const message = err instanceof Error ? err.message : t('customFieldsFailed');
|
|
3950
|
+
setError(message);
|
|
3951
|
+
} finally {
|
|
3952
|
+
setCustomFieldsLoading(false);
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3667
3956
|
// Handle delivery method selection
|
|
3668
3957
|
async function handleDeliveryTypeSelect(method: 'shipping' | 'pickup') {
|
|
3669
3958
|
if (!checkout) return;
|
|
@@ -3709,7 +3998,7 @@ function CheckoutContent() {
|
|
|
3709
3998
|
phone: customerInfo.phone,
|
|
3710
3999
|
});
|
|
3711
4000
|
setCheckout(updated);
|
|
3712
|
-
setStep(
|
|
4001
|
+
setStep(await loadCustomFieldsOrSkip(updated.id));
|
|
3713
4002
|
} catch (err) {
|
|
3714
4003
|
const message = err instanceof Error ? err.message : t('failedToSelectPickup');
|
|
3715
4004
|
setError(message);
|
|
@@ -3771,9 +4060,15 @@ function CheckoutContent() {
|
|
|
3771
4060
|
);
|
|
3772
4061
|
}
|
|
3773
4062
|
|
|
4063
|
+
const customFieldsStep =
|
|
4064
|
+
customFields.length > 0
|
|
4065
|
+
? [{ key: 'custom-fields' as CheckoutStep, label: t('stepCustomFields') }]
|
|
4066
|
+
: [];
|
|
4067
|
+
|
|
3774
4068
|
const steps: { key: CheckoutStep; label: string }[] = isAllDigital
|
|
3775
4069
|
? [
|
|
3776
4070
|
{ key: 'address', label: t('stepContactInfo') },
|
|
4071
|
+
...customFieldsStep,
|
|
3777
4072
|
{ key: 'payment', label: t('stepPayment') },
|
|
3778
4073
|
]
|
|
3779
4074
|
: pickupLocations.length > 0
|
|
@@ -3781,17 +4076,20 @@ function CheckoutContent() {
|
|
|
3781
4076
|
? [
|
|
3782
4077
|
{ key: 'method', label: t('stepMethod') },
|
|
3783
4078
|
{ key: 'pickup', label: t('stepPickup') },
|
|
4079
|
+
...customFieldsStep,
|
|
3784
4080
|
{ key: 'payment', label: t('stepPayment') },
|
|
3785
4081
|
]
|
|
3786
4082
|
: [
|
|
3787
4083
|
{ key: 'method', label: t('stepMethod') },
|
|
3788
4084
|
{ key: 'address', label: t('stepAddress') },
|
|
3789
4085
|
{ key: 'shipping', label: t('stepShipping') },
|
|
4086
|
+
...customFieldsStep,
|
|
3790
4087
|
{ key: 'payment', label: t('stepPayment') },
|
|
3791
4088
|
]
|
|
3792
4089
|
: [
|
|
3793
4090
|
{ key: 'address', label: t('stepAddress') },
|
|
3794
4091
|
{ key: 'shipping', label: t('stepShipping') },
|
|
4092
|
+
...customFieldsStep,
|
|
3795
4093
|
{ key: 'payment', label: t('stepPayment') },
|
|
3796
4094
|
];
|
|
3797
4095
|
|
|
@@ -3990,19 +4288,54 @@ function CheckoutContent() {
|
|
|
3990
4288
|
</div>
|
|
3991
4289
|
)}
|
|
3992
4290
|
|
|
4291
|
+
{/* Custom Fields (optional, between shipping/pickup and payment) */}
|
|
4292
|
+
{step === 'custom-fields' && checkout && (
|
|
4293
|
+
<div>
|
|
4294
|
+
<div className="mb-4 flex items-center justify-between">
|
|
4295
|
+
<h2 className="text-foreground text-lg font-semibold">{t('customFieldsTitle')}</h2>
|
|
4296
|
+
<button
|
|
4297
|
+
type="button"
|
|
4298
|
+
onClick={() => setStep(deliveryType === 'pickup' ? 'pickup' : 'shipping')}
|
|
4299
|
+
className="text-primary text-sm hover:underline"
|
|
4300
|
+
>
|
|
4301
|
+
{deliveryType === 'pickup' ? t('changePickup') : t('changeShipping')}
|
|
4302
|
+
</button>
|
|
4303
|
+
</div>
|
|
4304
|
+
<CustomFieldsStep
|
|
4305
|
+
fields={customFields}
|
|
4306
|
+
values={customFieldValues}
|
|
4307
|
+
onChange={(key, value) =>
|
|
4308
|
+
setCustomFieldValues((prev) => ({ ...prev, [key]: value }))
|
|
4309
|
+
}
|
|
4310
|
+
onApply={handleCustomFieldsApply}
|
|
4311
|
+
loading={customFieldsLoading}
|
|
4312
|
+
/>
|
|
4313
|
+
</div>
|
|
4314
|
+
)}
|
|
4315
|
+
|
|
3993
4316
|
{/* Payment */}
|
|
3994
4317
|
{step === 'payment' && checkout && (
|
|
3995
4318
|
<div>
|
|
3996
4319
|
<div className="mb-4 flex items-center justify-between">
|
|
3997
4320
|
<h2 className="text-foreground text-lg font-semibold">{t('payment')}</h2>
|
|
3998
|
-
{
|
|
4321
|
+
{customFields.length > 0 ? (
|
|
3999
4322
|
<button
|
|
4000
4323
|
type="button"
|
|
4001
|
-
onClick={() => setStep(
|
|
4324
|
+
onClick={() => setStep('custom-fields')}
|
|
4002
4325
|
className="text-primary text-sm hover:underline"
|
|
4003
4326
|
>
|
|
4004
|
-
{
|
|
4327
|
+
{t('changeOptions')}
|
|
4005
4328
|
</button>
|
|
4329
|
+
) : (
|
|
4330
|
+
!isAllDigital && (
|
|
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
|
+
)
|
|
4006
4339
|
)}
|
|
4007
4340
|
</div>
|
|
4008
4341
|
|
|
@@ -4189,6 +4522,20 @@ function CheckoutContent() {
|
|
|
4189
4522
|
taxBreakdown={checkout.taxBreakdown}
|
|
4190
4523
|
/>
|
|
4191
4524
|
|
|
4525
|
+
{/* Custom field surcharges (one line per applied surcharge) */}
|
|
4526
|
+
{checkout.appliedSurcharges && checkout.appliedSurcharges.length > 0 && (
|
|
4527
|
+
<>
|
|
4528
|
+
{checkout.appliedSurcharges.map((s) => (
|
|
4529
|
+
<div key={s.key} className="flex items-center justify-between">
|
|
4530
|
+
<span className="text-muted-foreground">{s.name}</span>
|
|
4531
|
+
<span className="text-foreground">
|
|
4532
|
+
{formatPrice(Number(s.amount), { currency }) as string}
|
|
4533
|
+
</span>
|
|
4534
|
+
</div>
|
|
4535
|
+
))}
|
|
4536
|
+
</>
|
|
4537
|
+
)}
|
|
4538
|
+
|
|
4192
4539
|
<div className="border-border mt-2 border-t pt-2">
|
|
4193
4540
|
<div className="flex items-center justify-between">
|
|
4194
4541
|
<span className="text-foreground font-semibold">{tc('total')}</span>
|
|
@@ -11244,8 +11591,11 @@ async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production)
|
|
|
11244
11591
|
} catch {
|
|
11245
11592
|
throw new Error(`Invalid response from ${baseUrl}`);
|
|
11246
11593
|
}
|
|
11594
|
+
const storeName = json.storeName || json.name || "My Store";
|
|
11595
|
+
const displayName = json.channelName || storeName;
|
|
11247
11596
|
return {
|
|
11248
|
-
name:
|
|
11597
|
+
name: displayName,
|
|
11598
|
+
storeName,
|
|
11249
11599
|
currency: json.currency || "USD",
|
|
11250
11600
|
language: json.language || "en",
|
|
11251
11601
|
...json.i18n ? { i18n: json.i18n } : {}
|
|
@@ -11286,7 +11636,7 @@ function getCandidateApiUrls() {
|
|
|
11286
11636
|
|
|
11287
11637
|
// src/tools/get-store-info.ts
|
|
11288
11638
|
var GET_STORE_INFO_NAME = "get-store-info";
|
|
11289
|
-
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.";
|
|
11639
|
+
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.";
|
|
11290
11640
|
var GET_STORE_INFO_SCHEMA = {
|
|
11291
11641
|
connectionId: z5.string().describe("Vibe-coded connection ID (starts with vc_)")
|
|
11292
11642
|
};
|
|
@@ -11299,7 +11649,10 @@ async function handleGetStoreInfo(args) {
|
|
|
11299
11649
|
type: "text",
|
|
11300
11650
|
text: JSON.stringify(
|
|
11301
11651
|
{
|
|
11652
|
+
// `name` is the channel display name (from the dashboard sales-channel card);
|
|
11653
|
+
// falls back to storeName when a channel doesn't have its own name.
|
|
11302
11654
|
name: resolved.info.name,
|
|
11655
|
+
storeName: resolved.info.storeName,
|
|
11303
11656
|
currency: resolved.info.currency,
|
|
11304
11657
|
language: resolved.info.language,
|
|
11305
11658
|
connectionId: args.connectionId,
|
|
@@ -11393,7 +11746,8 @@ var GET_STORE_CAPABILITIES_SCHEMA = {
|
|
|
11393
11746
|
};
|
|
11394
11747
|
function formatCapabilities(caps) {
|
|
11395
11748
|
const lines = [];
|
|
11396
|
-
|
|
11749
|
+
const channelSuffix = caps.store.channelName && caps.store.channelName !== caps.store.name ? ` \u2014 Channel: "${caps.store.channelName}"` : "";
|
|
11750
|
+
lines.push(`## Store: ${caps.store.name} (${caps.store.currency})${channelSuffix}`);
|
|
11397
11751
|
lines.push(`Language: ${caps.store.language}`);
|
|
11398
11752
|
if (caps.store.i18n?.enabled) {
|
|
11399
11753
|
lines.push(
|
|
@@ -11426,6 +11780,9 @@ function formatCapabilities(caps) {
|
|
|
11426
11780
|
caps.features.hasDownloadableProducts ? "\u2705 Downloadable products available" : "\u274C No downloadable products"
|
|
11427
11781
|
);
|
|
11428
11782
|
lines.push(caps.features.hasCoupons ? "\u2705 Coupons available" : "\u274C No coupons");
|
|
11783
|
+
lines.push(
|
|
11784
|
+
caps.features.hasCheckoutCustomFields ? "\u2705 Checkout custom fields configured (surcharges may apply)" : "\u274C No checkout custom fields"
|
|
11785
|
+
);
|
|
11429
11786
|
lines.push("");
|
|
11430
11787
|
lines.push("## Connection Settings");
|
|
11431
11788
|
lines.push(
|
|
@@ -11471,6 +11828,11 @@ function formatCapabilities(caps) {
|
|
|
11471
11828
|
if (caps.features.hasCoupons) {
|
|
11472
11829
|
suggestions.push('Add a coupon input to the cart. Use get-code-example("coupon-input").');
|
|
11473
11830
|
}
|
|
11831
|
+
if (caps.features.hasCheckoutCustomFields) {
|
|
11832
|
+
suggestions.push(
|
|
11833
|
+
`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").`
|
|
11834
|
+
);
|
|
11835
|
+
}
|
|
11474
11836
|
if (caps.connection.requireEmailVerification) {
|
|
11475
11837
|
suggestions.push(
|
|
11476
11838
|
'Email verification is required \u2014 ALWAYS build the verify-email page. Use get-code-example("verify-email").'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brainerce/mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "MCP server for AI-powered Brainerce store building. Provides SDK docs, types, and code examples to AI tools like Lovable, Cursor, and Claude Code.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"brainerce-mcp": "dist/bin/stdio.js"
|