@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/stdio.js
CHANGED
|
@@ -409,6 +409,234 @@ function PayPalPaymentForm({ clientId, orderId, checkoutId }: { clientId: string
|
|
|
409
409
|
}
|
|
410
410
|
\`\`\``;
|
|
411
411
|
}
|
|
412
|
+
function getCheckoutCustomFieldsSection() {
|
|
413
|
+
return `## Checkout Custom Fields (Optional Step Between Shipping & Payment)
|
|
414
|
+
|
|
415
|
+
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.
|
|
416
|
+
|
|
417
|
+
### When to Build This
|
|
418
|
+
|
|
419
|
+
Check capabilities first:
|
|
420
|
+
\`\`\`typescript
|
|
421
|
+
const caps = await fetch(\`/api/vc/\${connectionId}/capabilities\`).then(r => r.json());
|
|
422
|
+
if (caps.features.hasCheckoutCustomFields) {
|
|
423
|
+
// Build the step described below
|
|
424
|
+
}
|
|
425
|
+
\`\`\`
|
|
426
|
+
|
|
427
|
+
If \`hasCheckoutCustomFields\` is true, you MUST build this step or customers will be charged surcharges they never saw.
|
|
428
|
+
|
|
429
|
+
### Where to Place It
|
|
430
|
+
|
|
431
|
+
After \`selectShippingMethod()\` (or pickup confirmation), BEFORE \`createPaymentIntent()\`. Pseudocode:
|
|
432
|
+
|
|
433
|
+
\`\`\`
|
|
434
|
+
address \u2192 shipping/pickup \u2192 custom-fields (NEW) \u2192 payment
|
|
435
|
+
\`\`\`
|
|
436
|
+
|
|
437
|
+
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.
|
|
438
|
+
|
|
439
|
+
### Loading Field Definitions
|
|
440
|
+
|
|
441
|
+
\`\`\`typescript
|
|
442
|
+
import type { CheckoutCustomFieldDefinition } from 'brainerce';
|
|
443
|
+
|
|
444
|
+
const fields = await client.getCheckoutCustomFields(checkoutId);
|
|
445
|
+
if (fields.length === 0) {
|
|
446
|
+
setStep('payment'); // Nothing to collect
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
\`\`\`
|
|
450
|
+
|
|
451
|
+
Each \`CheckoutCustomFieldDefinition\` has:
|
|
452
|
+
- \`key\` \u2014 the form field name to use in your state
|
|
453
|
+
- \`name\` \u2014 display label
|
|
454
|
+
- \`description\` \u2014 helper text (render below the input)
|
|
455
|
+
- \`type\` \u2014 \`'TEXT' | 'TEXTAREA' | 'NUMBER' | 'BOOLEAN' | 'SELECT' | 'DATE'\`
|
|
456
|
+
- \`required\` \u2014 boolean
|
|
457
|
+
- \`minLength\` / \`maxLength\` \u2014 for TEXT/TEXTAREA
|
|
458
|
+
- \`minValue\` / \`maxValue\` \u2014 for NUMBER
|
|
459
|
+
- \`options\` \u2014 \`Array<{value, label}>\` for SELECT
|
|
460
|
+
- \`pricing\` \u2014 never render this; the server uses it to compute surcharges
|
|
461
|
+
|
|
462
|
+
### Rendering All 6 Field Types
|
|
463
|
+
|
|
464
|
+
\`\`\`typescript
|
|
465
|
+
function CustomFieldsStep({ fields, values, onChange, onApply, loading }: {
|
|
466
|
+
fields: CheckoutCustomFieldDefinition[];
|
|
467
|
+
values: Record<string, unknown>;
|
|
468
|
+
onChange: (key: string, value: unknown) => void;
|
|
469
|
+
onApply: () => void;
|
|
470
|
+
loading?: boolean;
|
|
471
|
+
}) {
|
|
472
|
+
return (
|
|
473
|
+
<div className="space-y-4">
|
|
474
|
+
{fields.map((field) => {
|
|
475
|
+
const value = values[field.key] ?? '';
|
|
476
|
+
const label = (
|
|
477
|
+
<label className="block text-sm font-medium">
|
|
478
|
+
{field.name}{field.required && <span className="text-red-500"> *</span>}
|
|
479
|
+
</label>
|
|
480
|
+
);
|
|
481
|
+
const help = field.description && (
|
|
482
|
+
<p className="text-xs text-gray-500 mt-1">{field.description}</p>
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
switch (field.type) {
|
|
486
|
+
case 'TEXT':
|
|
487
|
+
return (
|
|
488
|
+
<div key={field.key}>
|
|
489
|
+
{label}
|
|
490
|
+
<input
|
|
491
|
+
type="text"
|
|
492
|
+
value={value as string}
|
|
493
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
494
|
+
required={field.required}
|
|
495
|
+
minLength={field.minLength ?? undefined}
|
|
496
|
+
maxLength={field.maxLength ?? undefined}
|
|
497
|
+
className="w-full border rounded px-3 py-2"
|
|
498
|
+
/>
|
|
499
|
+
{help}
|
|
500
|
+
</div>
|
|
501
|
+
);
|
|
502
|
+
case 'TEXTAREA':
|
|
503
|
+
return (
|
|
504
|
+
<div key={field.key}>
|
|
505
|
+
{label}
|
|
506
|
+
<textarea
|
|
507
|
+
value={value as string}
|
|
508
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
509
|
+
required={field.required}
|
|
510
|
+
minLength={field.minLength ?? undefined}
|
|
511
|
+
maxLength={field.maxLength ?? undefined}
|
|
512
|
+
rows={3}
|
|
513
|
+
className="w-full border rounded px-3 py-2"
|
|
514
|
+
/>
|
|
515
|
+
{help}
|
|
516
|
+
</div>
|
|
517
|
+
);
|
|
518
|
+
case 'NUMBER':
|
|
519
|
+
return (
|
|
520
|
+
<div key={field.key}>
|
|
521
|
+
{label}
|
|
522
|
+
<input
|
|
523
|
+
type="number"
|
|
524
|
+
value={value as number}
|
|
525
|
+
onChange={(e) => onChange(field.key, e.target.valueAsNumber)}
|
|
526
|
+
required={field.required}
|
|
527
|
+
min={field.minValue ?? undefined}
|
|
528
|
+
max={field.maxValue ?? undefined}
|
|
529
|
+
className="w-full border rounded px-3 py-2"
|
|
530
|
+
/>
|
|
531
|
+
{help}
|
|
532
|
+
</div>
|
|
533
|
+
);
|
|
534
|
+
case 'BOOLEAN':
|
|
535
|
+
return (
|
|
536
|
+
<div key={field.key} className="flex items-start gap-2">
|
|
537
|
+
<input
|
|
538
|
+
type="checkbox"
|
|
539
|
+
checked={value === true}
|
|
540
|
+
onChange={(e) => onChange(field.key, e.target.checked)}
|
|
541
|
+
className="mt-1"
|
|
542
|
+
/>
|
|
543
|
+
<div>{label}{help}</div>
|
|
544
|
+
</div>
|
|
545
|
+
);
|
|
546
|
+
case 'SELECT':
|
|
547
|
+
return (
|
|
548
|
+
<div key={field.key}>
|
|
549
|
+
{label}
|
|
550
|
+
<select
|
|
551
|
+
value={value as string}
|
|
552
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
553
|
+
required={field.required}
|
|
554
|
+
className="w-full border rounded px-3 py-2"
|
|
555
|
+
>
|
|
556
|
+
<option value="">\u2014 Select \u2014</option>
|
|
557
|
+
{field.options?.map((opt) => (
|
|
558
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
559
|
+
))}
|
|
560
|
+
</select>
|
|
561
|
+
{help}
|
|
562
|
+
</div>
|
|
563
|
+
);
|
|
564
|
+
case 'DATE':
|
|
565
|
+
return (
|
|
566
|
+
<div key={field.key}>
|
|
567
|
+
{label}
|
|
568
|
+
<input
|
|
569
|
+
type="date"
|
|
570
|
+
value={value as string}
|
|
571
|
+
onChange={(e) => onChange(field.key, e.target.value)}
|
|
572
|
+
required={field.required}
|
|
573
|
+
className="w-full border rounded px-3 py-2"
|
|
574
|
+
/>
|
|
575
|
+
{help}
|
|
576
|
+
</div>
|
|
577
|
+
);
|
|
578
|
+
default:
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
})}
|
|
582
|
+
|
|
583
|
+
<button
|
|
584
|
+
type="button"
|
|
585
|
+
onClick={onApply}
|
|
586
|
+
disabled={loading}
|
|
587
|
+
className="w-full bg-black text-white py-3 rounded"
|
|
588
|
+
>
|
|
589
|
+
{loading ? 'Updating\u2026' : 'Continue to Payment'}
|
|
590
|
+
</button>
|
|
591
|
+
</div>
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
\`\`\`
|
|
595
|
+
|
|
596
|
+
### Submitting Values & Recalculating Surcharges
|
|
597
|
+
|
|
598
|
+
\`\`\`typescript
|
|
599
|
+
async function applyCustomFields() {
|
|
600
|
+
setLoading(true);
|
|
601
|
+
try {
|
|
602
|
+
// Server validates required + min/max + coerces types + recalculates totals
|
|
603
|
+
const updated = await client.setCheckoutCustomFields(checkoutId, customFieldValues);
|
|
604
|
+
setCheckout(updated); // updated.surchargeAmount and updated.appliedSurcharges are now fresh
|
|
605
|
+
setStep('payment');
|
|
606
|
+
} catch (err) {
|
|
607
|
+
setError(err instanceof Error ? err.message : 'Failed to save selections');
|
|
608
|
+
} finally {
|
|
609
|
+
setLoading(false);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
\`\`\`
|
|
613
|
+
|
|
614
|
+
### Order Summary \u2014 Show the Surcharges
|
|
615
|
+
|
|
616
|
+
**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:
|
|
617
|
+
|
|
618
|
+
\`\`\`typescript
|
|
619
|
+
{checkout.appliedSurcharges?.length > 0 && (
|
|
620
|
+
<>
|
|
621
|
+
{checkout.appliedSurcharges.map((s) => (
|
|
622
|
+
<div key={s.key} className="flex justify-between text-sm text-gray-600">
|
|
623
|
+
<span>{s.name}</span>
|
|
624
|
+
<span>{formatPrice(s.amount, { currency })}</span>
|
|
625
|
+
</div>
|
|
626
|
+
))}
|
|
627
|
+
</>
|
|
628
|
+
)}
|
|
629
|
+
\`\`\`
|
|
630
|
+
|
|
631
|
+
\`checkout.surchargeAmount\` is a string (use \`parseFloat()\`); \`checkout.totalAmount\` already includes it.
|
|
632
|
+
|
|
633
|
+
### Notes
|
|
634
|
+
|
|
635
|
+
- The server filters fields by \`visibility\` (\`always\` / \`shipping\` / \`pickup\` / \`products\`) \u2014 your client just renders what \`getCheckoutCustomFields()\` returns.
|
|
636
|
+
- 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.
|
|
637
|
+
- If \`getCheckoutCustomFields()\` returns 0 fields, skip the step entirely. Don't render an empty UI.
|
|
638
|
+
`;
|
|
639
|
+
}
|
|
412
640
|
function getPaymentProvidersSection() {
|
|
413
641
|
return `## Payment Provider Detection (DO THIS FIRST on checkout page!)
|
|
414
642
|
|
|
@@ -1258,6 +1486,10 @@ ${getCheckoutFlowSection(currency)}
|
|
|
1258
1486
|
|
|
1259
1487
|
---
|
|
1260
1488
|
|
|
1489
|
+
${getCheckoutCustomFieldsSection()}
|
|
1490
|
+
|
|
1491
|
+
---
|
|
1492
|
+
|
|
1261
1493
|
${getPaymentProvidersSection()}
|
|
1262
1494
|
|
|
1263
1495
|
---
|
|
@@ -1316,6 +1548,8 @@ function getSectionByTopic(topic, connectionId, currency) {
|
|
|
1316
1548
|
return getCartSection(cur);
|
|
1317
1549
|
case "checkout":
|
|
1318
1550
|
return getCheckoutFlowSection(cur);
|
|
1551
|
+
case "checkout-custom-fields":
|
|
1552
|
+
return getCheckoutCustomFieldsSection();
|
|
1319
1553
|
case "payment":
|
|
1320
1554
|
return getPaymentProvidersSection();
|
|
1321
1555
|
case "auth":
|
|
@@ -1344,7 +1578,7 @@ function getSectionByTopic(topic, connectionId, currency) {
|
|
|
1344
1578
|
storeType: "E-commerce store"
|
|
1345
1579
|
});
|
|
1346
1580
|
default:
|
|
1347
|
-
return `Unknown topic: "${topic}". Available topics: setup, products, cart, checkout, payment, auth, order-confirmation, inventory, discounts, recommendations, tax, critical-rules, type-reference, admin, all`;
|
|
1581
|
+
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`;
|
|
1348
1582
|
}
|
|
1349
1583
|
}
|
|
1350
1584
|
|
|
@@ -1357,6 +1591,7 @@ var GET_SDK_DOCS_SCHEMA = {
|
|
|
1357
1591
|
"products",
|
|
1358
1592
|
"cart",
|
|
1359
1593
|
"checkout",
|
|
1594
|
+
"checkout-custom-fields",
|
|
1360
1595
|
"payment",
|
|
1361
1596
|
"auth",
|
|
1362
1597
|
"order-confirmation",
|
|
@@ -3378,6 +3613,7 @@ import type {
|
|
|
3378
3613
|
ShippingDestinations,
|
|
3379
3614
|
PickupLocation,
|
|
3380
3615
|
CheckoutBumpsResponse,
|
|
3616
|
+
CheckoutCustomFieldDefinition,
|
|
3381
3617
|
} from 'brainerce';
|
|
3382
3618
|
import { formatPrice } from 'brainerce';
|
|
3383
3619
|
import { getClient } from '@/lib/brainerce';
|
|
@@ -3387,6 +3623,7 @@ import { ShippingStep } from '@/components/checkout/shipping-step';
|
|
|
3387
3623
|
import { PaymentStep } from '@/components/checkout/payment-step';
|
|
3388
3624
|
import { DeliveryMethodStep } from '@/components/checkout/delivery-method-step';
|
|
3389
3625
|
import { PickupStep } from '@/components/checkout/pickup-step';
|
|
3626
|
+
import { CustomFieldsStep } from '@/components/checkout/custom-fields-step';
|
|
3390
3627
|
import { TaxDisplay } from '@/components/checkout/tax-display';
|
|
3391
3628
|
import { OrderBumpCard } from '@/components/checkout/order-bump-card';
|
|
3392
3629
|
import { CouponInput } from '@/components/cart/coupon-input';
|
|
@@ -3395,7 +3632,7 @@ import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
|
3395
3632
|
import { useTranslations } from '@/lib/translations';
|
|
3396
3633
|
import { cn } from '@/lib/utils';
|
|
3397
3634
|
|
|
3398
|
-
type CheckoutStep = 'method' | 'address' | 'shipping' | 'pickup' | 'payment';
|
|
3635
|
+
type CheckoutStep = 'method' | 'address' | 'shipping' | 'pickup' | 'custom-fields' | 'payment';
|
|
3399
3636
|
|
|
3400
3637
|
function CheckoutContent() {
|
|
3401
3638
|
const searchParams = useSearchParams();
|
|
@@ -3428,6 +3665,9 @@ function CheckoutContent() {
|
|
|
3428
3665
|
const [orderBumps, setOrderBumps] = useState<CheckoutBumpsResponse | null>(null);
|
|
3429
3666
|
const [addedBumpIds, setAddedBumpIds] = useState<Set<string>>(new Set());
|
|
3430
3667
|
const [bumpLoading, setBumpLoading] = useState<string | null>(null);
|
|
3668
|
+
const [customFields, setCustomFields] = useState<CheckoutCustomFieldDefinition[]>([]);
|
|
3669
|
+
const [customFieldValues, setCustomFieldValues] = useState<Record<string, unknown>>({});
|
|
3670
|
+
const [customFieldsLoading, setCustomFieldsLoading] = useState(false);
|
|
3431
3671
|
|
|
3432
3672
|
// Check for returning from canceled payment
|
|
3433
3673
|
const canceled = searchParams.get('canceled') === 'true';
|
|
@@ -3479,6 +3719,23 @@ function CheckoutContent() {
|
|
|
3479
3719
|
const existing = await client.getCheckout(existingCheckoutId);
|
|
3480
3720
|
setCheckout(existing);
|
|
3481
3721
|
|
|
3722
|
+
// Preload custom field definitions and any existing values so the
|
|
3723
|
+
// step indicator and "change options" affordance work on resume.
|
|
3724
|
+
client
|
|
3725
|
+
.getCheckoutCustomFields(existing.id)
|
|
3726
|
+
.then((fields) => {
|
|
3727
|
+
setCustomFields(fields);
|
|
3728
|
+
const existingValues = (
|
|
3729
|
+
existing as unknown as {
|
|
3730
|
+
customFieldValues?: Record<string, unknown> | null;
|
|
3731
|
+
}
|
|
3732
|
+
).customFieldValues;
|
|
3733
|
+
if (existingValues) setCustomFieldValues(existingValues);
|
|
3734
|
+
})
|
|
3735
|
+
.catch(() => {
|
|
3736
|
+
setCustomFields([]);
|
|
3737
|
+
});
|
|
3738
|
+
|
|
3482
3739
|
// Determine step based on checkout state
|
|
3483
3740
|
const allDigital = existing.lineItems.every(
|
|
3484
3741
|
(i) => (i.product as unknown as { isDownloadable?: boolean }).isDownloadable
|
|
@@ -3649,6 +3906,21 @@ function CheckoutContent() {
|
|
|
3649
3906
|
}
|
|
3650
3907
|
}
|
|
3651
3908
|
|
|
3909
|
+
// After shipping/pickup is set, decide whether to show the custom-fields step
|
|
3910
|
+
// or jump straight to payment. Returns the next step.
|
|
3911
|
+
async function loadCustomFieldsOrSkip(checkoutId: string): Promise<CheckoutStep> {
|
|
3912
|
+
try {
|
|
3913
|
+
const fields = await getClient().getCheckoutCustomFields(checkoutId);
|
|
3914
|
+
setCustomFields(fields);
|
|
3915
|
+
return fields.length > 0 ? 'custom-fields' : 'payment';
|
|
3916
|
+
} catch {
|
|
3917
|
+
// If the endpoint isn't available or fails, fall through to payment
|
|
3918
|
+
// rather than blocking the customer.
|
|
3919
|
+
setCustomFields([]);
|
|
3920
|
+
return 'payment';
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
|
|
3652
3924
|
// Handle shipping method selection
|
|
3653
3925
|
async function handleShippingSelect(rateId: string) {
|
|
3654
3926
|
if (!checkout) return;
|
|
@@ -3661,7 +3933,7 @@ function CheckoutContent() {
|
|
|
3661
3933
|
|
|
3662
3934
|
const updated = await client.selectShippingMethod(checkout.id, rateId);
|
|
3663
3935
|
setCheckout(updated);
|
|
3664
|
-
setStep(
|
|
3936
|
+
setStep(await loadCustomFieldsOrSkip(updated.id));
|
|
3665
3937
|
} catch (err) {
|
|
3666
3938
|
const message = err instanceof Error ? err.message : t('failedToSelectShipping');
|
|
3667
3939
|
setError(message);
|
|
@@ -3670,6 +3942,23 @@ function CheckoutContent() {
|
|
|
3670
3942
|
}
|
|
3671
3943
|
}
|
|
3672
3944
|
|
|
3945
|
+
// Submit custom fields
|
|
3946
|
+
async function handleCustomFieldsApply() {
|
|
3947
|
+
if (!checkout) return;
|
|
3948
|
+
try {
|
|
3949
|
+
setCustomFieldsLoading(true);
|
|
3950
|
+
setError(null);
|
|
3951
|
+
const updated = await getClient().setCheckoutCustomFields(checkout.id, customFieldValues);
|
|
3952
|
+
setCheckout(updated);
|
|
3953
|
+
setStep('payment');
|
|
3954
|
+
} catch (err) {
|
|
3955
|
+
const message = err instanceof Error ? err.message : t('customFieldsFailed');
|
|
3956
|
+
setError(message);
|
|
3957
|
+
} finally {
|
|
3958
|
+
setCustomFieldsLoading(false);
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
|
|
3673
3962
|
// Handle delivery method selection
|
|
3674
3963
|
async function handleDeliveryTypeSelect(method: 'shipping' | 'pickup') {
|
|
3675
3964
|
if (!checkout) return;
|
|
@@ -3715,7 +4004,7 @@ function CheckoutContent() {
|
|
|
3715
4004
|
phone: customerInfo.phone,
|
|
3716
4005
|
});
|
|
3717
4006
|
setCheckout(updated);
|
|
3718
|
-
setStep(
|
|
4007
|
+
setStep(await loadCustomFieldsOrSkip(updated.id));
|
|
3719
4008
|
} catch (err) {
|
|
3720
4009
|
const message = err instanceof Error ? err.message : t('failedToSelectPickup');
|
|
3721
4010
|
setError(message);
|
|
@@ -3777,9 +4066,15 @@ function CheckoutContent() {
|
|
|
3777
4066
|
);
|
|
3778
4067
|
}
|
|
3779
4068
|
|
|
4069
|
+
const customFieldsStep =
|
|
4070
|
+
customFields.length > 0
|
|
4071
|
+
? [{ key: 'custom-fields' as CheckoutStep, label: t('stepCustomFields') }]
|
|
4072
|
+
: [];
|
|
4073
|
+
|
|
3780
4074
|
const steps: { key: CheckoutStep; label: string }[] = isAllDigital
|
|
3781
4075
|
? [
|
|
3782
4076
|
{ key: 'address', label: t('stepContactInfo') },
|
|
4077
|
+
...customFieldsStep,
|
|
3783
4078
|
{ key: 'payment', label: t('stepPayment') },
|
|
3784
4079
|
]
|
|
3785
4080
|
: pickupLocations.length > 0
|
|
@@ -3787,17 +4082,20 @@ function CheckoutContent() {
|
|
|
3787
4082
|
? [
|
|
3788
4083
|
{ key: 'method', label: t('stepMethod') },
|
|
3789
4084
|
{ key: 'pickup', label: t('stepPickup') },
|
|
4085
|
+
...customFieldsStep,
|
|
3790
4086
|
{ key: 'payment', label: t('stepPayment') },
|
|
3791
4087
|
]
|
|
3792
4088
|
: [
|
|
3793
4089
|
{ key: 'method', label: t('stepMethod') },
|
|
3794
4090
|
{ key: 'address', label: t('stepAddress') },
|
|
3795
4091
|
{ key: 'shipping', label: t('stepShipping') },
|
|
4092
|
+
...customFieldsStep,
|
|
3796
4093
|
{ key: 'payment', label: t('stepPayment') },
|
|
3797
4094
|
]
|
|
3798
4095
|
: [
|
|
3799
4096
|
{ key: 'address', label: t('stepAddress') },
|
|
3800
4097
|
{ key: 'shipping', label: t('stepShipping') },
|
|
4098
|
+
...customFieldsStep,
|
|
3801
4099
|
{ key: 'payment', label: t('stepPayment') },
|
|
3802
4100
|
];
|
|
3803
4101
|
|
|
@@ -3996,19 +4294,54 @@ function CheckoutContent() {
|
|
|
3996
4294
|
</div>
|
|
3997
4295
|
)}
|
|
3998
4296
|
|
|
4297
|
+
{/* Custom Fields (optional, between shipping/pickup and payment) */}
|
|
4298
|
+
{step === 'custom-fields' && checkout && (
|
|
4299
|
+
<div>
|
|
4300
|
+
<div className="mb-4 flex items-center justify-between">
|
|
4301
|
+
<h2 className="text-foreground text-lg font-semibold">{t('customFieldsTitle')}</h2>
|
|
4302
|
+
<button
|
|
4303
|
+
type="button"
|
|
4304
|
+
onClick={() => setStep(deliveryType === 'pickup' ? 'pickup' : 'shipping')}
|
|
4305
|
+
className="text-primary text-sm hover:underline"
|
|
4306
|
+
>
|
|
4307
|
+
{deliveryType === 'pickup' ? t('changePickup') : t('changeShipping')}
|
|
4308
|
+
</button>
|
|
4309
|
+
</div>
|
|
4310
|
+
<CustomFieldsStep
|
|
4311
|
+
fields={customFields}
|
|
4312
|
+
values={customFieldValues}
|
|
4313
|
+
onChange={(key, value) =>
|
|
4314
|
+
setCustomFieldValues((prev) => ({ ...prev, [key]: value }))
|
|
4315
|
+
}
|
|
4316
|
+
onApply={handleCustomFieldsApply}
|
|
4317
|
+
loading={customFieldsLoading}
|
|
4318
|
+
/>
|
|
4319
|
+
</div>
|
|
4320
|
+
)}
|
|
4321
|
+
|
|
3999
4322
|
{/* Payment */}
|
|
4000
4323
|
{step === 'payment' && checkout && (
|
|
4001
4324
|
<div>
|
|
4002
4325
|
<div className="mb-4 flex items-center justify-between">
|
|
4003
4326
|
<h2 className="text-foreground text-lg font-semibold">{t('payment')}</h2>
|
|
4004
|
-
{
|
|
4327
|
+
{customFields.length > 0 ? (
|
|
4005
4328
|
<button
|
|
4006
4329
|
type="button"
|
|
4007
|
-
onClick={() => setStep(
|
|
4330
|
+
onClick={() => setStep('custom-fields')}
|
|
4008
4331
|
className="text-primary text-sm hover:underline"
|
|
4009
4332
|
>
|
|
4010
|
-
{
|
|
4333
|
+
{t('changeOptions')}
|
|
4011
4334
|
</button>
|
|
4335
|
+
) : (
|
|
4336
|
+
!isAllDigital && (
|
|
4337
|
+
<button
|
|
4338
|
+
type="button"
|
|
4339
|
+
onClick={() => setStep(deliveryType === 'pickup' ? 'pickup' : 'shipping')}
|
|
4340
|
+
className="text-primary text-sm hover:underline"
|
|
4341
|
+
>
|
|
4342
|
+
{deliveryType === 'pickup' ? t('changePickup') : t('changeShipping')}
|
|
4343
|
+
</button>
|
|
4344
|
+
)
|
|
4012
4345
|
)}
|
|
4013
4346
|
</div>
|
|
4014
4347
|
|
|
@@ -4195,6 +4528,20 @@ function CheckoutContent() {
|
|
|
4195
4528
|
taxBreakdown={checkout.taxBreakdown}
|
|
4196
4529
|
/>
|
|
4197
4530
|
|
|
4531
|
+
{/* Custom field surcharges (one line per applied surcharge) */}
|
|
4532
|
+
{checkout.appliedSurcharges && checkout.appliedSurcharges.length > 0 && (
|
|
4533
|
+
<>
|
|
4534
|
+
{checkout.appliedSurcharges.map((s) => (
|
|
4535
|
+
<div key={s.key} className="flex items-center justify-between">
|
|
4536
|
+
<span className="text-muted-foreground">{s.name}</span>
|
|
4537
|
+
<span className="text-foreground">
|
|
4538
|
+
{formatPrice(Number(s.amount), { currency }) as string}
|
|
4539
|
+
</span>
|
|
4540
|
+
</div>
|
|
4541
|
+
))}
|
|
4542
|
+
</>
|
|
4543
|
+
)}
|
|
4544
|
+
|
|
4198
4545
|
<div className="border-border mt-2 border-t pt-2">
|
|
4199
4546
|
<div className="flex items-center justify-between">
|
|
4200
4547
|
<span className="text-foreground font-semibold">{tc('total')}</span>
|
|
@@ -11250,8 +11597,11 @@ async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production)
|
|
|
11250
11597
|
} catch {
|
|
11251
11598
|
throw new Error(`Invalid response from ${baseUrl}`);
|
|
11252
11599
|
}
|
|
11600
|
+
const storeName = json.storeName || json.name || "My Store";
|
|
11601
|
+
const displayName = json.channelName || storeName;
|
|
11253
11602
|
return {
|
|
11254
|
-
name:
|
|
11603
|
+
name: displayName,
|
|
11604
|
+
storeName,
|
|
11255
11605
|
currency: json.currency || "USD",
|
|
11256
11606
|
language: json.language || "en",
|
|
11257
11607
|
...json.i18n ? { i18n: json.i18n } : {}
|
|
@@ -11292,7 +11642,7 @@ function getCandidateApiUrls() {
|
|
|
11292
11642
|
|
|
11293
11643
|
// src/tools/get-store-info.ts
|
|
11294
11644
|
var GET_STORE_INFO_NAME = "get-store-info";
|
|
11295
|
-
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.";
|
|
11645
|
+
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.";
|
|
11296
11646
|
var GET_STORE_INFO_SCHEMA = {
|
|
11297
11647
|
connectionId: import_zod5.z.string().describe("Vibe-coded connection ID (starts with vc_)")
|
|
11298
11648
|
};
|
|
@@ -11305,7 +11655,10 @@ async function handleGetStoreInfo(args) {
|
|
|
11305
11655
|
type: "text",
|
|
11306
11656
|
text: JSON.stringify(
|
|
11307
11657
|
{
|
|
11658
|
+
// `name` is the channel display name (from the dashboard sales-channel card);
|
|
11659
|
+
// falls back to storeName when a channel doesn't have its own name.
|
|
11308
11660
|
name: resolved.info.name,
|
|
11661
|
+
storeName: resolved.info.storeName,
|
|
11309
11662
|
currency: resolved.info.currency,
|
|
11310
11663
|
language: resolved.info.language,
|
|
11311
11664
|
connectionId: args.connectionId,
|
|
@@ -11399,7 +11752,8 @@ var GET_STORE_CAPABILITIES_SCHEMA = {
|
|
|
11399
11752
|
};
|
|
11400
11753
|
function formatCapabilities(caps) {
|
|
11401
11754
|
const lines = [];
|
|
11402
|
-
|
|
11755
|
+
const channelSuffix = caps.store.channelName && caps.store.channelName !== caps.store.name ? ` \u2014 Channel: "${caps.store.channelName}"` : "";
|
|
11756
|
+
lines.push(`## Store: ${caps.store.name} (${caps.store.currency})${channelSuffix}`);
|
|
11403
11757
|
lines.push(`Language: ${caps.store.language}`);
|
|
11404
11758
|
if (caps.store.i18n?.enabled) {
|
|
11405
11759
|
lines.push(
|
|
@@ -11432,6 +11786,9 @@ function formatCapabilities(caps) {
|
|
|
11432
11786
|
caps.features.hasDownloadableProducts ? "\u2705 Downloadable products available" : "\u274C No downloadable products"
|
|
11433
11787
|
);
|
|
11434
11788
|
lines.push(caps.features.hasCoupons ? "\u2705 Coupons available" : "\u274C No coupons");
|
|
11789
|
+
lines.push(
|
|
11790
|
+
caps.features.hasCheckoutCustomFields ? "\u2705 Checkout custom fields configured (surcharges may apply)" : "\u274C No checkout custom fields"
|
|
11791
|
+
);
|
|
11435
11792
|
lines.push("");
|
|
11436
11793
|
lines.push("## Connection Settings");
|
|
11437
11794
|
lines.push(
|
|
@@ -11477,6 +11834,11 @@ function formatCapabilities(caps) {
|
|
|
11477
11834
|
if (caps.features.hasCoupons) {
|
|
11478
11835
|
suggestions.push('Add a coupon input to the cart. Use get-code-example("coupon-input").');
|
|
11479
11836
|
}
|
|
11837
|
+
if (caps.features.hasCheckoutCustomFields) {
|
|
11838
|
+
suggestions.push(
|
|
11839
|
+
`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").`
|
|
11840
|
+
);
|
|
11841
|
+
}
|
|
11480
11842
|
if (caps.connection.requireEmailVerification) {
|
|
11481
11843
|
suggestions.push(
|
|
11482
11844
|
'Email verification is required \u2014 ALWAYS build the verify-email page. Use get-code-example("verify-email").'
|
package/dist/index.d.mts
CHANGED
|
@@ -33,7 +33,14 @@ declare const AVAILABLE_DOMAINS: string[];
|
|
|
33
33
|
declare const PROJECT_TEMPLATE = "# Next.js Brainerce Store \u2014 Project Structure\n\n```\nmy-store/\n\u251C\u2500\u2500 package.json # Dependencies: brainerce, next, react, tailwindcss\n\u251C\u2500\u2500 next.config.js\n\u251C\u2500\u2500 tailwind.config.ts\n\u251C\u2500\u2500 tsconfig.json\n\u251C\u2500\u2500 lib/\n\u2502 \u2514\u2500\u2500 brainerce.ts # SDK client setup, cart helpers, auth helpers\n\u251C\u2500\u2500 app/\n\u2502 \u251C\u2500\u2500 layout.tsx # Root layout with StoreProvider\n\u2502 \u251C\u2500\u2500 page.tsx # Home: hero, featured products, categories\n\u2502 \u251C\u2500\u2500 products/\n\u2502 \u2502 \u251C\u2500\u2500 page.tsx # Product listing with infinite scroll\n\u2502 \u2502 \u2514\u2500\u2500 [slug]/\n\u2502 \u2502 \u2514\u2500\u2500 page.tsx # Product detail: images, variants, add to cart\n\u2502 \u251C\u2500\u2500 cart/\n\u2502 \u2502 \u2514\u2500\u2500 page.tsx # Cart: items with checkboxes, coupon, totals\n\u2502 \u251C\u2500\u2500 checkout/\n\u2502 \u2502 \u2514\u2500\u2500 page.tsx # Checkout: address \u2192 shipping \u2192 payment\n\u2502 \u251C\u2500\u2500 order-confirmation/\n\u2502 \u2502 \u2514\u2500\u2500 page.tsx # Order confirmation: waitForOrder() + handlePaymentSuccess()\n\u2502 \u251C\u2500\u2500 login/\n\u2502 \u2502 \u2514\u2500\u2500 page.tsx # Login: email/password + OAuth buttons\n\u2502 \u251C\u2500\u2500 register/\n\u2502 \u2502 \u2514\u2500\u2500 page.tsx # Register: form + OAuth buttons\n\u2502 \u251C\u2500\u2500 verify-email/\n\u2502 \u2502 \u2514\u2500\u2500 page.tsx # Email verification: 6-digit code + resend\n\u2502 \u251C\u2500\u2500 auth/\n\u2502 \u2502 \u2514\u2500\u2500 callback/\n\u2502 \u2502 \u2514\u2500\u2500 page.tsx # OAuth callback: extract token from URL params\n\u2502 \u2514\u2500\u2500 account/\n\u2502 \u2514\u2500\u2500 page.tsx # Account: profile + order history (protected)\n\u251C\u2500\u2500 components/\n\u2502 \u251C\u2500\u2500 Header.tsx # Logo, nav, cart icon, search autocomplete\n\u2502 \u251C\u2500\u2500 ProductCard.tsx # Product card with image, price, stock badge\n\u2502 \u251C\u2500\u2500 StockBadge.tsx # \"Only X left!\" low stock indicator\n\u2502 \u251C\u2500\u2500 ReservationCountdown.tsx # Timer for reserved items\n\u2502 \u251C\u2500\u2500 StripePaymentForm.tsx # Stripe Elements payment form\n\u2502 \u251C\u2500\u2500 GrowPaymentForm.tsx # Grow iframe payment form\n\u2502 \u2514\u2500\u2500 PayPalPaymentForm.tsx # PayPal Buttons payment form\n\u2514\u2500\u2500 public/\n \u2514\u2500\u2500 placeholder.jpg # Fallback product image\n```\n\n## Key Dependencies\n\n```json\n{\n \"dependencies\": {\n \"brainerce\": \"latest\",\n \"next\": \"^15\",\n \"react\": \"^19\",\n \"react-dom\": \"^19\",\n \"@stripe/stripe-js\": \"latest\",\n \"@stripe/react-stripe-js\": \"latest\",\n \"@paypal/react-paypal-js\": \"latest\"\n },\n \"devDependencies\": {\n \"tailwindcss\": \"^4\",\n \"typescript\": \"^5\",\n \"@types/react\": \"latest\",\n \"@types/node\": \"latest\"\n }\n}\n```\n\n## File Descriptions\n\n| File | Purpose |\n|------|---------|\n| `lib/brainerce.ts` | SDK client init, cart ID helpers, customer token helpers |\n| `app/page.tsx` | Homepage with hero section and featured products grid |\n| `app/products/page.tsx` | Product listing with infinite scroll (50/page), filters, search |\n| `app/products/[slug]/page.tsx` | Product detail with variant switching, description, metafields |\n| `app/cart/page.tsx` | Cart with checkboxes for partial checkout, coupon input, reservation timer |\n| `app/checkout/page.tsx` | Multi-step: address \u2192 shipping rates \u2192 payment (Stripe/Grow/PayPal) |\n| `app/order-confirmation/page.tsx` | Polls waitForOrder(), shows order number, clears cart |\n| `app/login/page.tsx` | Email/password login + OAuth buttons, handles requiresVerification |\n| `app/register/page.tsx` | Registration form + OAuth, handles requiresVerification |\n| `app/verify-email/page.tsx` | 6-digit verification code input with resend button |\n| `app/auth/callback/page.tsx` | Extracts OAuth token from URL params, stores in localStorage |\n| `app/account/page.tsx` | Protected page: getMyProfile() + getMyOrders() |\n| `components/Header.tsx` | Site header with nav, cart count badge, search autocomplete |\n\n## Content Security Policy (CSP)\n\nPayment SDKs load iframes and scripts from external domains. Add these CSP headers in `next.config.js`:\n\n```js\n// next.config.js\nmodule.exports = {\n async headers() {\n return [{\n source: '/(.*)',\n headers: [{\n key: 'Content-Security-Policy',\n value: [\n \"default-src 'self'\",\n \"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.meshulam.co.il https://meshulam.co.il https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://js.stripe.com https://pay.google.com\",\n \"style-src 'self' 'unsafe-inline'\",\n \"img-src 'self' data: blob: https:\",\n \"font-src 'self' data:\",\n \"frame-src 'self' https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://*.creditguard.co.il https://js.stripe.com https://hooks.stripe.com https://pay.google.com\",\n \"connect-src 'self' https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://google.com https://pay.google.com https://*.stripe.com https://*.creditguard.co.il\",\n \"worker-src 'self' blob:\",\n ].join('; '),\n }],\n }];\n },\n};\n```\n\n**Required domains by provider:**\n- **Grow**: `*.meshulam.co.il`, `grow.link`, `*.grow.link`, `*.grow.security`, `*.creditguard.co.il`\n- **Stripe**: `js.stripe.com`, `hooks.stripe.com`, `*.stripe.com`\n- **Google Pay**: `pay.google.com`, `google.com`\n";
|
|
34
34
|
|
|
35
35
|
interface StoreInfo {
|
|
36
|
+
/**
|
|
37
|
+
* Display name — prefers the per-connection channel name (what the user
|
|
38
|
+
* sees in their Brainerce dashboard sales-channel card), falling back to
|
|
39
|
+
* the parent store name.
|
|
40
|
+
*/
|
|
36
41
|
name: string;
|
|
42
|
+
/** Parent store name (the account-level store). */
|
|
43
|
+
storeName: string;
|
|
37
44
|
currency: string;
|
|
38
45
|
language: string;
|
|
39
46
|
i18n?: {
|
|
@@ -46,7 +53,10 @@ declare function fetchStoreInfo(connectionId: string, baseUrl?: string): Promise
|
|
|
46
53
|
|
|
47
54
|
interface StoreCapabilities {
|
|
48
55
|
store: {
|
|
56
|
+
/** Parent store name (account-level). */
|
|
49
57
|
name: string;
|
|
58
|
+
/** Per-connection channel display name (what the user sees in the dashboard). */
|
|
59
|
+
channelName?: string;
|
|
50
60
|
currency: string;
|
|
51
61
|
language: string;
|
|
52
62
|
i18n?: {
|
|
@@ -79,6 +89,7 @@ interface StoreCapabilities {
|
|
|
79
89
|
hasDiscountRules: boolean;
|
|
80
90
|
hasDownloadableProducts: boolean;
|
|
81
91
|
hasCoupons: boolean;
|
|
92
|
+
hasCheckoutCustomFields: boolean;
|
|
82
93
|
};
|
|
83
94
|
}
|
|
84
95
|
declare function fetchStoreCapabilities(connectionId: string, baseUrl?: string): Promise<StoreCapabilities>;
|
package/dist/index.d.ts
CHANGED
|
@@ -33,7 +33,14 @@ declare const AVAILABLE_DOMAINS: string[];
|
|
|
33
33
|
declare const PROJECT_TEMPLATE = "# Next.js Brainerce Store \u2014 Project Structure\n\n```\nmy-store/\n\u251C\u2500\u2500 package.json # Dependencies: brainerce, next, react, tailwindcss\n\u251C\u2500\u2500 next.config.js\n\u251C\u2500\u2500 tailwind.config.ts\n\u251C\u2500\u2500 tsconfig.json\n\u251C\u2500\u2500 lib/\n\u2502 \u2514\u2500\u2500 brainerce.ts # SDK client setup, cart helpers, auth helpers\n\u251C\u2500\u2500 app/\n\u2502 \u251C\u2500\u2500 layout.tsx # Root layout with StoreProvider\n\u2502 \u251C\u2500\u2500 page.tsx # Home: hero, featured products, categories\n\u2502 \u251C\u2500\u2500 products/\n\u2502 \u2502 \u251C\u2500\u2500 page.tsx # Product listing with infinite scroll\n\u2502 \u2502 \u2514\u2500\u2500 [slug]/\n\u2502 \u2502 \u2514\u2500\u2500 page.tsx # Product detail: images, variants, add to cart\n\u2502 \u251C\u2500\u2500 cart/\n\u2502 \u2502 \u2514\u2500\u2500 page.tsx # Cart: items with checkboxes, coupon, totals\n\u2502 \u251C\u2500\u2500 checkout/\n\u2502 \u2502 \u2514\u2500\u2500 page.tsx # Checkout: address \u2192 shipping \u2192 payment\n\u2502 \u251C\u2500\u2500 order-confirmation/\n\u2502 \u2502 \u2514\u2500\u2500 page.tsx # Order confirmation: waitForOrder() + handlePaymentSuccess()\n\u2502 \u251C\u2500\u2500 login/\n\u2502 \u2502 \u2514\u2500\u2500 page.tsx # Login: email/password + OAuth buttons\n\u2502 \u251C\u2500\u2500 register/\n\u2502 \u2502 \u2514\u2500\u2500 page.tsx # Register: form + OAuth buttons\n\u2502 \u251C\u2500\u2500 verify-email/\n\u2502 \u2502 \u2514\u2500\u2500 page.tsx # Email verification: 6-digit code + resend\n\u2502 \u251C\u2500\u2500 auth/\n\u2502 \u2502 \u2514\u2500\u2500 callback/\n\u2502 \u2502 \u2514\u2500\u2500 page.tsx # OAuth callback: extract token from URL params\n\u2502 \u2514\u2500\u2500 account/\n\u2502 \u2514\u2500\u2500 page.tsx # Account: profile + order history (protected)\n\u251C\u2500\u2500 components/\n\u2502 \u251C\u2500\u2500 Header.tsx # Logo, nav, cart icon, search autocomplete\n\u2502 \u251C\u2500\u2500 ProductCard.tsx # Product card with image, price, stock badge\n\u2502 \u251C\u2500\u2500 StockBadge.tsx # \"Only X left!\" low stock indicator\n\u2502 \u251C\u2500\u2500 ReservationCountdown.tsx # Timer for reserved items\n\u2502 \u251C\u2500\u2500 StripePaymentForm.tsx # Stripe Elements payment form\n\u2502 \u251C\u2500\u2500 GrowPaymentForm.tsx # Grow iframe payment form\n\u2502 \u2514\u2500\u2500 PayPalPaymentForm.tsx # PayPal Buttons payment form\n\u2514\u2500\u2500 public/\n \u2514\u2500\u2500 placeholder.jpg # Fallback product image\n```\n\n## Key Dependencies\n\n```json\n{\n \"dependencies\": {\n \"brainerce\": \"latest\",\n \"next\": \"^15\",\n \"react\": \"^19\",\n \"react-dom\": \"^19\",\n \"@stripe/stripe-js\": \"latest\",\n \"@stripe/react-stripe-js\": \"latest\",\n \"@paypal/react-paypal-js\": \"latest\"\n },\n \"devDependencies\": {\n \"tailwindcss\": \"^4\",\n \"typescript\": \"^5\",\n \"@types/react\": \"latest\",\n \"@types/node\": \"latest\"\n }\n}\n```\n\n## File Descriptions\n\n| File | Purpose |\n|------|---------|\n| `lib/brainerce.ts` | SDK client init, cart ID helpers, customer token helpers |\n| `app/page.tsx` | Homepage with hero section and featured products grid |\n| `app/products/page.tsx` | Product listing with infinite scroll (50/page), filters, search |\n| `app/products/[slug]/page.tsx` | Product detail with variant switching, description, metafields |\n| `app/cart/page.tsx` | Cart with checkboxes for partial checkout, coupon input, reservation timer |\n| `app/checkout/page.tsx` | Multi-step: address \u2192 shipping rates \u2192 payment (Stripe/Grow/PayPal) |\n| `app/order-confirmation/page.tsx` | Polls waitForOrder(), shows order number, clears cart |\n| `app/login/page.tsx` | Email/password login + OAuth buttons, handles requiresVerification |\n| `app/register/page.tsx` | Registration form + OAuth, handles requiresVerification |\n| `app/verify-email/page.tsx` | 6-digit verification code input with resend button |\n| `app/auth/callback/page.tsx` | Extracts OAuth token from URL params, stores in localStorage |\n| `app/account/page.tsx` | Protected page: getMyProfile() + getMyOrders() |\n| `components/Header.tsx` | Site header with nav, cart count badge, search autocomplete |\n\n## Content Security Policy (CSP)\n\nPayment SDKs load iframes and scripts from external domains. Add these CSP headers in `next.config.js`:\n\n```js\n// next.config.js\nmodule.exports = {\n async headers() {\n return [{\n source: '/(.*)',\n headers: [{\n key: 'Content-Security-Policy',\n value: [\n \"default-src 'self'\",\n \"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.meshulam.co.il https://meshulam.co.il https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://js.stripe.com https://pay.google.com\",\n \"style-src 'self' 'unsafe-inline'\",\n \"img-src 'self' data: blob: https:\",\n \"font-src 'self' data:\",\n \"frame-src 'self' https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://*.creditguard.co.il https://js.stripe.com https://hooks.stripe.com https://pay.google.com\",\n \"connect-src 'self' https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://google.com https://pay.google.com https://*.stripe.com https://*.creditguard.co.il\",\n \"worker-src 'self' blob:\",\n ].join('; '),\n }],\n }];\n },\n};\n```\n\n**Required domains by provider:**\n- **Grow**: `*.meshulam.co.il`, `grow.link`, `*.grow.link`, `*.grow.security`, `*.creditguard.co.il`\n- **Stripe**: `js.stripe.com`, `hooks.stripe.com`, `*.stripe.com`\n- **Google Pay**: `pay.google.com`, `google.com`\n";
|
|
34
34
|
|
|
35
35
|
interface StoreInfo {
|
|
36
|
+
/**
|
|
37
|
+
* Display name — prefers the per-connection channel name (what the user
|
|
38
|
+
* sees in their Brainerce dashboard sales-channel card), falling back to
|
|
39
|
+
* the parent store name.
|
|
40
|
+
*/
|
|
36
41
|
name: string;
|
|
42
|
+
/** Parent store name (the account-level store). */
|
|
43
|
+
storeName: string;
|
|
37
44
|
currency: string;
|
|
38
45
|
language: string;
|
|
39
46
|
i18n?: {
|
|
@@ -46,7 +53,10 @@ declare function fetchStoreInfo(connectionId: string, baseUrl?: string): Promise
|
|
|
46
53
|
|
|
47
54
|
interface StoreCapabilities {
|
|
48
55
|
store: {
|
|
56
|
+
/** Parent store name (account-level). */
|
|
49
57
|
name: string;
|
|
58
|
+
/** Per-connection channel display name (what the user sees in the dashboard). */
|
|
59
|
+
channelName?: string;
|
|
50
60
|
currency: string;
|
|
51
61
|
language: string;
|
|
52
62
|
i18n?: {
|
|
@@ -79,6 +89,7 @@ interface StoreCapabilities {
|
|
|
79
89
|
hasDiscountRules: boolean;
|
|
80
90
|
hasDownloadableProducts: boolean;
|
|
81
91
|
hasCoupons: boolean;
|
|
92
|
+
hasCheckoutCustomFields: boolean;
|
|
82
93
|
};
|
|
83
94
|
}
|
|
84
95
|
declare function fetchStoreCapabilities(connectionId: string, baseUrl?: string): Promise<StoreCapabilities>;
|