@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/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('payment');
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('payment');
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
- {!isAllDigital && (
4327
+ {customFields.length > 0 ? (
4005
4328
  <button
4006
4329
  type="button"
4007
- onClick={() => setStep(deliveryType === 'pickup' ? 'pickup' : 'shipping')}
4330
+ onClick={() => setStep('custom-fields')}
4008
4331
  className="text-primary text-sm hover:underline"
4009
4332
  >
4010
- {deliveryType === 'pickup' ? t('changePickup') : t('changeShipping')}
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: json.name || json.storeName || "My Store",
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
- lines.push(`## Store: ${caps.store.name} (${caps.store.currency})`);
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>;