brainerce 1.38.0 → 1.41.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/README.md CHANGED
@@ -400,7 +400,7 @@ if (!page) notFound();
400
400
 
401
401
  All `get` / `getBySlug` return `null` on 404 — render a hard-coded fallback so the page never crashes when the merchant hasn't seeded yet.
402
402
 
403
- **Security**: `FAQ.items[i].answer`, `RICH_TEXT.html`, and `PAGE.html` are **merchant-authored HTML**. The server does NOT pre-sanitize (merchants may embed iframes). ALWAYS sanitize before injecting:
403
+ **Security**: `FAQ.items[i].answer`, `RICH_TEXT.html`, `PAGE.html`, and `Product.description` are **merchant-authored HTML**. The server does NOT pre-sanitize FAQ/RICH_TEXT/PAGE (merchants may embed iframes); `Product.description` is server-sanitized on write but you still sanitize on render. `Product.description` may contain `<video>` and host-locked YouTube/Vimeo `<iframe>` embeds — allow those tags (iframe restricted to `www.youtube.com` / `www.youtube-nocookie.com` / `player.vimeo.com`) and add those hosts to your CSP `frame-src`. ALWAYS sanitize before injecting:
404
404
 
405
405
  ```typescript
406
406
  import DOMPurify from 'isomorphic-dompurify';
@@ -1526,10 +1526,19 @@ fields.forEach((field) => {
1526
1526
  // field.required: whether customer must fill this in
1527
1527
  // field.minLength, field.maxLength: validation for text fields
1528
1528
  // field.minValue, field.maxValue: validation for number fields
1529
- // field.enumValues: allowed options for dropdown fields
1529
+ // field.enumValues: CustomizationFieldOption[] for SELECT/MULTI_SELECT
1530
+ // each option: { label: string, value: string, swatchColor?: string, swatchImageUrl?: string }
1531
+ // use option.value for metadata submission, option.label for display
1530
1532
  // field.defaultValue: default value to pre-fill
1531
1533
  });
1532
1534
 
1535
+ // Render SELECT options using label/value (not string[] anymore):
1536
+ for (const option of field.enumValues ?? []) {
1537
+ // option.label → show to user (e.g. "Gold")
1538
+ // option.value → submit in metadata (e.g. "gold")
1539
+ // option.swatchColor → hex color for color swatch UI, if set
1540
+ }
1541
+
1533
1542
  // Customer fills in values, then add to cart with metadata
1534
1543
  await client.addToCart(cartId, {
1535
1544
  productId: product.id,
@@ -1537,10 +1546,23 @@ await client.addToCart(cartId, {
1537
1546
  metadata: {
1538
1547
  cake_text: 'Happy Birthday!', // TEXT field
1539
1548
  font_color: '#FF0000', // COLOR field
1549
+ frame_color: 'gold', // SELECT — use option.value
1540
1550
  },
1541
1551
  });
1542
1552
  ```
1543
1553
 
1554
+ **Display customizations in cart/checkout — no extra API call needed:**
1555
+
1556
+ `CartItem` and `CheckoutLineItem` both include a `customizations` object with resolved labels. Use it directly — no need to call `getProduct()` per item.
1557
+
1558
+ ```typescript
1559
+ // Works for CartItem, CheckoutLineItem, and OrderItem — same shape
1560
+ const summary = Object.values(item.customizations ?? {})
1561
+ .map(c => `${c.label}: ${Array.isArray(c.value) ? c.value.join(', ') : c.value}`)
1562
+ .join(' | ');
1563
+ // e.g. "Frame color: Gold | Add-ons: Gift wrap, Engraving"
1564
+ ```
1565
+
1544
1566
  **For IMAGE/GALLERY fields**, upload the file first:
1545
1567
 
1546
1568
  ```typescript
@@ -2353,7 +2375,16 @@ const checkoutId = checkout.id;
2353
2375
 
2354
2376
  #### Get All Payment Providers (Recommended)
2355
2377
 
2356
- Use this method to get ALL enabled payment providers and build dynamic UI:
2378
+ Use this method to get ALL enabled payment providers and build dynamic UI.
2379
+
2380
+ **Primary vs. additive methods (Shopify-parity).** Each provider carries a
2381
+ `methodType`. A `CREDIT_CARD` provider is the *primary* card processor — the
2382
+ single method that settles the order (`defaultProvider`, `isAdditive: false`,
2383
+ `presentation: 'card_form'`). Everything else is *additive* (`isAdditive: true`),
2384
+ e.g. PayPal is a `'WALLET'` with `presentation: 'express_button'`. Render additive
2385
+ methods as accelerated-checkout **express buttons above the card form** — they sit
2386
+ *alongside* the primary, never replace it. (Exception: a wallet-only store has no
2387
+ card processor, so its wallet becomes the `defaultProvider` and stands alone.)
2357
2388
 
2358
2389
  ```typescript
2359
2390
  const { hasPayments, providers, defaultProvider } = await client.getPaymentProviders();
@@ -2369,7 +2400,10 @@ const { hasPayments, providers, defaultProvider } = await client.getPaymentProvi
2369
2400
  // publicKey: 'pk_live_xxx...',
2370
2401
  // supportedMethods: ['card', 'ideal'],
2371
2402
  // testMode: false,
2372
- // isDefault: true
2403
+ // isDefault: true,
2404
+ // methodType: 'CREDIT_CARD', // primary card processor
2405
+ // presentation: 'card_form',
2406
+ // isAdditive: false
2373
2407
  // },
2374
2408
  // {
2375
2409
  // id: 'provider_yyy',
@@ -2378,12 +2412,20 @@ const { hasPayments, providers, defaultProvider } = await client.getPaymentProvi
2378
2412
  // publicKey: 'client_id_xxx...',
2379
2413
  // supportedMethods: ['paypal'],
2380
2414
  // testMode: false,
2381
- // isDefault: false
2415
+ // isDefault: false,
2416
+ // methodType: 'WALLET', // additive — render as an express button
2417
+ // presentation: 'express_button',
2418
+ // isAdditive: true
2382
2419
  // }
2383
2420
  // ],
2384
- // defaultProvider: { ... } // The default provider (first one)
2421
+ // defaultProvider: { ... } // The settling primary (never an additive wallet
2422
+ // // while a card processor is installed)
2385
2423
  // }
2386
2424
 
2425
+ // Recommended pattern: express buttons (additive) above the primary card form.
2426
+ const expressMethods = providers.filter(p => p.isAdditive); // e.g. PayPal
2427
+ const primary = defaultProvider && !defaultProvider.isAdditive ? defaultProvider : undefined;
2428
+
2387
2429
  // Build dynamic UI based on available providers
2388
2430
  if (!hasPayments) {
2389
2431
  return <div>Payment not configured for this store</div>;
@@ -3402,6 +3444,8 @@ client.trackEvent({ eventType: 'engagement', path: '/products/shoes', engagedMs:
3402
3444
 
3403
3445
  > `trackEvent()` is fire-and-forget — errors are silently swallowed so a failed beacon never breaks your storefront. Available in `salesChannelId` and `storeId` modes.
3404
3446
 
3447
+ > **Analytics always goes directly to the Brainerce API** (`keepalive` beacon), even when you route the rest of the SDK through a same-origin BFF proxy (`proxyMode` / `baseUrl: '/api/store'`). This is deliberate: a beacon relayed through your server would arrive from the server's datacenter IP and mis-geolocate **every** visitor to that region (e.g. a Vercel US function → all visitors show as US). When `proxyMode` is on — or `baseUrl` shares the page origin — the SDK auto-targets `https://api.brainerce.com` for beacons; override with `analyticsBaseUrl` only if your API is self-hosted.
3448
+
3405
3449
  **Privacy:** cookieless, no PII stored. The visitor IP is resolved to a country server-side and then discarded. No cookie-consent banner required under GDPR/ePrivacy.
3406
3450
 
3407
3451
  ---
@@ -3550,6 +3594,26 @@ await client.createZoneShippingRate('zone_id', {
3550
3594
  });
3551
3595
  ```
3552
3596
 
3597
+ ### App Store Shipping (Shippo)
3598
+
3599
+ Once a merchant installs the Shippo app from the Brainerce App Store and connects their own
3600
+ Shippo account, live carrier rates appear automatically at checkout. Billing goes directly
3601
+ to the merchant's Shippo account.
3602
+
3603
+ ```typescript
3604
+ // After an order is placed, purchase a shipping label:
3605
+ const label = await admin.createShippingLabel(orderId, {
3606
+ shippoRateObjectId: 'rate_8f123456789abcdef', // Shippo rate.object_id from checkout rates
3607
+ });
3608
+
3609
+ console.log(label.labelUrl); // PDF label URL for printing
3610
+ console.log(label.trackingNumber); // Auto-populated on the order
3611
+ console.log(label.carrier); // e.g. 'USPS', 'UPS', 'FedEx'
3612
+ ```
3613
+
3614
+ The `trackingNumber` is stored on the `Order` automatically — customers see it in their
3615
+ order history without any extra integration work.
3616
+
3553
3617
  ### Tax Configuration
3554
3618
 
3555
3619
  `rate` is a **whole percentage** (`7.25` = 7.25%, not `0.0725`). A rate may target
package/dist/index.d.mts CHANGED
@@ -112,6 +112,32 @@ interface BrainerceClientOptions {
112
112
  * ```
113
113
  */
114
114
  proxyMode?: boolean;
115
+ /**
116
+ * Base URL used for analytics beacons (`trackEvent`) ONLY — everything else
117
+ * keeps using `baseUrl`.
118
+ *
119
+ * Analytics must reach the Brainerce API **directly from the browser** so the
120
+ * visitor's real IP — and therefore country — is preserved. If a beacon is routed
121
+ * through a same-origin BFF proxy (see `proxyMode`), it arrives at the backend from
122
+ * the proxy server's datacenter IP instead (e.g. a Vercel US region), and every
123
+ * visitor is mis-geolocated to that datacenter.
124
+ *
125
+ * You normally do NOT need to set this: when `proxyMode` is on (or `baseUrl` shares
126
+ * the current page's origin), the SDK automatically sends analytics to
127
+ * `https://api.brainerce.com`. Set it explicitly only when your Brainerce API lives
128
+ * at a custom / self-hosted origin.
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * const client = new BrainerceClient({
133
+ * connectionId: 'vc_abc123...',
134
+ * baseUrl: '/api/store', // products / cart / auth go through the BFF proxy
135
+ * proxyMode: true,
136
+ * // analytics auto-routes direct to https://api.brainerce.com — no config needed
137
+ * });
138
+ * ```
139
+ */
140
+ analyticsBaseUrl?: string;
115
141
  }
116
142
  interface StoreInfo {
117
143
  id: string;
@@ -256,13 +282,20 @@ interface Product {
256
282
  */
257
283
  localeSlugs?: Record<string, string> | null;
258
284
  /**
259
- * Product description as HTML. **Always sanitize before rendering**, e.g.:
285
+ * Product description as HTML. May contain self-hosted `<video>` and
286
+ * host-locked YouTube/Vimeo `<iframe>` embeds. **Always sanitize before
287
+ * rendering**, e.g.:
260
288
  *
261
289
  * import { sanitizeHTML } from '@brainerce/utils';
262
290
  * <div dangerouslySetInnerHTML={{ __html: sanitizeHTML(product.description) }} />
263
291
  *
264
- * The backend strips dangerous tags/attributes on write, but storefronts should
265
- * still treat product description as untrusted HTML and sanitize on render.
292
+ * The backend strips dangerous tags/attributes on write, but storefronts
293
+ * should still treat this as untrusted HTML and sanitize on render. Your
294
+ * sanitizer must allow `video`/`source` and — for embeds — `iframe` restricted
295
+ * to the hosts `www.youtube.com`, `www.youtube-nocookie.com`,
296
+ * `player.vimeo.com` (never arbitrary iframes). Sizing is on the `width`/
297
+ * `height` attributes (inline `style` is stripped). If you set a CSP, add
298
+ * those three hosts to `frame-src` or embeds render blank.
266
299
  */
267
300
  description?: string | null;
268
301
  descriptionFormat?: 'text' | 'html' | 'markdown' | null;
@@ -1064,7 +1097,14 @@ interface CreateProductDto {
1064
1097
  status?: 'active' | 'draft';
1065
1098
  type?: 'SIMPLE' | 'VARIABLE';
1066
1099
  isDownloadable?: boolean;
1100
+ /** Existing category IDs to assign. Unknown/cross-store IDs are rejected with 400. To assign by name (auto-creating if missing), use `categoryNames`. */
1067
1101
  categories?: string[];
1102
+ /** Category names to assign — matched case-insensitively and auto-created if none exists. Merged with any IDs in `categories`. */
1103
+ categoryNames?: string[];
1104
+ /** Existing brand IDs to assign. Unknown/cross-store IDs are rejected with 400. To assign by name (auto-creating if missing), use `brandNames`. */
1105
+ brands?: string[];
1106
+ /** Brand names to assign — matched case-insensitively and auto-created if none exists. Merged with any IDs in `brands`. */
1107
+ brandNames?: string[];
1068
1108
  tags?: string[];
1069
1109
  images?: ProductImage[];
1070
1110
  }
@@ -1078,7 +1118,14 @@ interface UpdateProductDto {
1078
1118
  costPrice?: number | null;
1079
1119
  status?: 'active' | 'draft';
1080
1120
  isDownloadable?: boolean;
1121
+ /** Existing category IDs to assign (replaces the current set). Pass `[]` to clear. Unknown/cross-store IDs are rejected with 400. To assign by name (auto-creating if missing), use `categoryNames`. */
1081
1122
  categories?: string[];
1123
+ /** Category names to assign — matched case-insensitively and auto-created if missing. Additive: added to `categories` (or to the product's current categories when `categories` is omitted). */
1124
+ categoryNames?: string[];
1125
+ /** Existing brand IDs to assign (replaces the current set). Pass `[]` to clear. Unknown/cross-store IDs are rejected with 400. To assign by name (auto-creating if missing), use `brandNames`. */
1126
+ brands?: string[];
1127
+ /** Brand names to assign — matched case-insensitively and auto-created if missing. Additive: added to `brands` (or to the product's current brands when `brands` is omitted). */
1128
+ brandNames?: string[];
1082
1129
  tags?: string[];
1083
1130
  images?: ProductImage[];
1084
1131
  }
@@ -1806,6 +1853,20 @@ interface CartItem {
1806
1853
  notes?: string | null;
1807
1854
  /** Optional metadata for this line item */
1808
1855
  metadata?: Record<string, unknown> | null;
1856
+ /**
1857
+ * Resolved customer input field values with merchant-defined labels.
1858
+ * Mirrors `OrderItem.customizations` — same shape, live (not order-frozen).
1859
+ * Present only when the item has at least one customization value set.
1860
+ *
1861
+ * @example
1862
+ * // Display "Color: Silver / Size: 45" without an extra getProduct() call:
1863
+ * Object.entries(item.customizations ?? {}).map(([, c]) => `${c.label}: ${c.value}`)
1864
+ */
1865
+ customizations?: Record<string, {
1866
+ label: string;
1867
+ value: string | string[];
1868
+ type: string;
1869
+ }>;
1809
1870
  /**
1810
1871
  * Per-line modifier breakdown — present when the line was added with
1811
1872
  * modifier `selections`. Snapshot taken at add-time; immutable to catalog
@@ -2326,6 +2387,20 @@ interface CheckoutLineItem {
2326
2387
  notes?: string | null;
2327
2388
  /** Custom metadata including customer input field values */
2328
2389
  metadata?: Record<string, unknown> | null;
2390
+ /**
2391
+ * Resolved customer input field values with merchant-defined labels.
2392
+ * Mirrors `OrderItem.customizations` — same shape, live (not order-frozen).
2393
+ * Present only when the item has at least one customization value set.
2394
+ *
2395
+ * @example
2396
+ * // Render "Color: Silver / Ring size: 45" in your checkout summary:
2397
+ * Object.entries(item.customizations ?? {}).map(([, c]) => `${c.label}: ${c.value}`)
2398
+ */
2399
+ customizations?: Record<string, {
2400
+ label: string;
2401
+ value: string | string[];
2402
+ type: string;
2403
+ }>;
2329
2404
  }
2330
2405
  /**
2331
2406
  * Represents an available shipping rate/method for checkout.
@@ -3070,6 +3145,18 @@ interface PaymentClientSdk {
3070
3145
  *
3071
3146
  * **Alias:** `PaymentProvider` is the same type (use either name).
3072
3147
  */
3148
+ /**
3149
+ * Payment-method taxonomy (Shopify-parity). `CREDIT_CARD` is a *primary* card
3150
+ * processor — the single method that settles the order; everything else is an
3151
+ * *additive* method (wallet/alt/local/redeemable) that coexists alongside it.
3152
+ */
3153
+ type PaymentMethodType = 'CREDIT_CARD' | 'WALLET' | 'OFFSITE' | 'LOCAL' | 'REDEEMABLE';
3154
+ /**
3155
+ * How a storefront should render a payment method. `card_form` = the main card
3156
+ * fields; `express_button` = an accelerated-checkout button (render it ABOVE the
3157
+ * card form, like PayPal / Apple Pay); `redirect` = send the buyer offsite.
3158
+ */
3159
+ type PaymentPresentation = 'card_form' | 'express_button' | 'redirect' | 'local_option' | 'redeemable_field';
3073
3160
  interface PaymentProviderConfig {
3074
3161
  /** Provider ID (use this when creating payment intents) */
3075
3162
  id: string;
@@ -3085,8 +3172,26 @@ interface PaymentProviderConfig {
3085
3172
  supportedMethods: string[];
3086
3173
  /** Whether this is test/sandbox mode */
3087
3174
  testMode: boolean;
3088
- /** Whether this is the default provider */
3175
+ /**
3176
+ * Whether this is the store's default *settling* provider. Only a primary
3177
+ * (`methodType: 'CREDIT_CARD'`) provider is ever the default — an additive
3178
+ * wallet never becomes the default while a card processor is installed.
3179
+ */
3089
3180
  isDefault: boolean;
3181
+ /**
3182
+ * Payment-method taxonomy — primary card processor vs additive wallet/alt.
3183
+ * Use it to place the method: primary → the main card form; additive → an
3184
+ * express button. See `presentation`.
3185
+ */
3186
+ methodType: PaymentMethodType;
3187
+ /** Rendering hint derived from `methodType` (card_form | express_button | …). */
3188
+ presentation: PaymentPresentation;
3189
+ /**
3190
+ * True for wallet/alternative methods (e.g. PayPal) that are shown *alongside*
3191
+ * the primary card processor rather than replacing it. Render these as
3192
+ * accelerated-checkout express buttons above the card form.
3193
+ */
3194
+ isAdditive: boolean;
3090
3195
  /** Client-side SDK configuration for providers that render a widget */
3091
3196
  clientSdk?: PaymentClientSdk;
3092
3197
  }
@@ -3983,7 +4088,8 @@ interface MetafieldDefinition {
3983
4088
  maxLength?: number | null;
3984
4089
  minValue?: number | null;
3985
4090
  maxValue?: number | null;
3986
- enumValues?: string[] | null;
4091
+ /** Allowed values for SELECT / MULTI_SELECT fields, with optional swatch metadata. */
4092
+ enumValues?: CustomizationFieldOption[] | null;
3987
4093
  defaultValue?: string | null;
3988
4094
  position: number;
3989
4095
  isActive: boolean;
@@ -4067,7 +4173,8 @@ interface PublicMetafieldDefinition {
4067
4173
  * Pair with `getProducts({ metafields: { [key]: [...] } })`.
4068
4174
  */
4069
4175
  filterable?: boolean;
4070
- enumValues?: string[];
4176
+ /** Allowed values for SELECT / MULTI_SELECT fields, with optional swatch metadata. */
4177
+ enumValues?: CustomizationFieldOption[];
4071
4178
  minLength?: number | null;
4072
4179
  maxLength?: number | null;
4073
4180
  minValue?: number | null;
@@ -4075,6 +4182,20 @@ interface PublicMetafieldDefinition {
4075
4182
  defaultValue?: string | null;
4076
4183
  position: number;
4077
4184
  }
4185
+ /**
4186
+ * A single selectable option for a SELECT or MULTI_SELECT customization field.
4187
+ * Legacy string-only `enumValues` arrays are automatically promoted to this shape.
4188
+ */
4189
+ interface CustomizationFieldOption {
4190
+ /** Human-readable label shown in the storefront UI (e.g., "Rose Gold"). */
4191
+ label: string;
4192
+ /** Value stored in `CartItem.metadata` and carried through to `OrderItem.customizations`. */
4193
+ value: string;
4194
+ /** Optional CSS hex color for rendering a color swatch (e.g., "#B76E79"). */
4195
+ swatchColor?: string | null;
4196
+ /** Optional image URL for rendering a texture/image swatch. */
4197
+ swatchImageUrl?: string | null;
4198
+ }
4078
4199
  /**
4079
4200
  * Customer-facing input field definition assigned to a specific product.
4080
4201
  * Returned in product responses to tell the storefront which input fields to render.
@@ -4090,7 +4211,11 @@ interface ProductCustomizationField {
4090
4211
  maxLength?: number | null;
4091
4212
  minValue?: number | null;
4092
4213
  maxValue?: number | null;
4093
- enumValues?: string[];
4214
+ /**
4215
+ * Allowed values for SELECT / MULTI_SELECT fields.
4216
+ * Each option includes a label, value, and optional swatch color/image.
4217
+ */
4218
+ enumValues?: CustomizationFieldOption[];
4094
4219
  defaultValue?: string | null;
4095
4220
  position: number;
4096
4221
  }
@@ -5379,6 +5504,8 @@ declare class BrainerceClient {
5379
5504
  private readonly storeId?;
5380
5505
  private readonly connectionId?;
5381
5506
  private readonly baseUrl;
5507
+ /** Direct API base for analytics beacons — bypasses any same-origin BFF proxy so the visitor's real IP (hence country) reaches the backend. */
5508
+ private readonly analyticsBaseUrl;
5382
5509
  private readonly timeout;
5383
5510
  private customerToken;
5384
5511
  private customerCartId;
@@ -5548,6 +5675,20 @@ declare class BrainerceClient {
5548
5675
  * ```
5549
5676
  */
5550
5677
  trackEvent(payload?: TrackEventPayload): Promise<void>;
5678
+ /**
5679
+ * Resolve the base URL for analytics beacons. Analytics must go DIRECTLY to the
5680
+ * Brainerce API (never through a same-origin BFF proxy) so the visitor's real IP —
5681
+ * hence country — reaches the backend. Explicit `analyticsBaseUrl` wins; otherwise
5682
+ * use the direct API when `baseUrl` is a same-origin proxy, else the (already-direct)
5683
+ * `baseUrl` (which may be a custom / self-hosted Brainerce API).
5684
+ */
5685
+ private resolveAnalyticsBaseUrl;
5686
+ /**
5687
+ * Fire-and-forget analytics beacon sent directly to `analyticsBaseUrl`. Uses
5688
+ * `keepalive` so it survives page unload (engagement events fired on pagehide),
5689
+ * omits credentials (cookieless), and never throws.
5690
+ */
5691
+ private sendAnalyticsBeacon;
5551
5692
  /**
5552
5693
  * Get a list of products with pagination and filtering
5553
5694
  * Works in vibe-coded, storefront (public), and admin mode
@@ -5972,6 +6113,29 @@ declare class BrainerceClient {
5972
6113
  * ```
5973
6114
  */
5974
6115
  updateOrderShipping(orderId: string, data: UpdateOrderShippingDto): Promise<Order>;
6116
+ /**
6117
+ * Create a shipping label for an order via the installed App Store shipping app.
6118
+ * Pass the rate ID returned from checkout rate shopping. Billing goes directly
6119
+ * to the merchant's carrier account — Brainerce is not a billing intermediary.
6120
+ *
6121
+ * @example
6122
+ * ```typescript
6123
+ * const label = await client.createShippingLabel('order_abc', {
6124
+ * rateId: 'rate_8f123456789abcdef',
6125
+ * });
6126
+ * console.log('Label URL:', label.labelUrl);
6127
+ * console.log('Tracking:', label.trackingNumber);
6128
+ * ```
6129
+ */
6130
+ createShippingLabel(orderId: string, data: {
6131
+ rateId: string;
6132
+ parcel?: Record<string, string>;
6133
+ }): Promise<{
6134
+ shipmentId: string;
6135
+ labelUrl: string;
6136
+ trackingNumber: string;
6137
+ carrier: string;
6138
+ }>;
5975
6139
  /**
5976
6140
  * Cancel an order
5977
6141
  * Works for Shopify and WooCommerce orders that haven't been fulfilled
@@ -7859,6 +8023,14 @@ declare class BrainerceClient {
7859
8023
  *
7860
8024
  * @returns Payment providers config with hasPayments flag and provider list
7861
8025
  *
8026
+ * Each provider carries a `methodType` (Shopify-parity taxonomy) and a derived
8027
+ * `isAdditive` / `presentation`. There is at most ONE primary card processor
8028
+ * (`methodType: 'CREDIT_CARD'`, the `defaultProvider`) that settles the order;
8029
+ * additive methods (`isAdditive: true`, e.g. PayPal as a `'WALLET'`) are shown
8030
+ * *alongside* it as accelerated-checkout express buttons — they never replace
8031
+ * the card form. A wallet-only store is the one exception: with no card
8032
+ * processor installed, the wallet becomes the `defaultProvider` and stands alone.
8033
+ *
7862
8034
  * @example
7863
8035
  * ```typescript
7864
8036
  * const { providers, hasPayments, defaultProvider } = await client.getPaymentProviders();
@@ -7868,27 +8040,21 @@ declare class BrainerceClient {
7868
8040
  * return;
7869
8041
  * }
7870
8042
  *
7871
- * // Find specific providers
7872
- * const stripeProvider = providers.find(p => p.provider === 'stripe');
7873
- * const paypalProvider = providers.find(p => p.provider === 'paypal');
7874
- *
7875
- * // Initialize Stripe if available (with Connect account support)
7876
- * if (stripeProvider) {
7877
- * const stripe = await loadStripe(stripeProvider.publicKey, {
7878
- * stripeAccount: stripeProvider.stripeAccountId, // For Stripe Connect
7879
- * });
7880
- * // Show Stripe payment form
7881
- * }
7882
- *
7883
- * // Initialize PayPal if available
7884
- * if (paypalProvider) {
7885
- * // Load PayPal SDK with paypalProvider.publicKey as client-id
7886
- * // Show PayPal buttons
8043
+ * // Render additive methods (wallets) as express buttons ABOVE the card form.
8044
+ * const expressMethods = providers.filter(p => p.isAdditive); // e.g. PayPal
8045
+ * for (const m of expressMethods) {
8046
+ * // m.presentation === 'express_button' — render an express/accelerated button
8047
+ * // and call createPaymentIntent({ providerId: m.id }) when the buyer taps it.
7887
8048
  * }
7888
8049
  *
7889
- * // Or use the default provider for a simpler single-provider flow
7890
- * if (defaultProvider) {
7891
- * console.log(`Using default: ${defaultProvider.name} (${defaultProvider.provider})`);
8050
+ * // Render the primary card processor (the settling default) as the main form.
8051
+ * if (defaultProvider && !defaultProvider.isAdditive) {
8052
+ * if (defaultProvider.provider === 'stripe') {
8053
+ * const stripe = await loadStripe(defaultProvider.publicKey, {
8054
+ * stripeAccount: defaultProvider.stripeAccountId, // For Stripe Connect
8055
+ * });
8056
+ * // Show the Stripe card form
8057
+ * }
7892
8058
  * }
7893
8059
  * ```
7894
8060
  */
@@ -9543,7 +9709,7 @@ declare class BrainerceError extends Error {
9543
9709
  constructor(message: string, statusCode: number, details?: unknown);
9544
9710
  }
9545
9711
 
9546
- declare const SDK_VERSION = "1.36.1";
9712
+ declare const SDK_VERSION = "1.41.0";
9547
9713
 
9548
9714
  /**
9549
9715
  * Verify a webhook signature from Brainerce
package/dist/index.d.ts CHANGED
@@ -112,6 +112,32 @@ interface BrainerceClientOptions {
112
112
  * ```
113
113
  */
114
114
  proxyMode?: boolean;
115
+ /**
116
+ * Base URL used for analytics beacons (`trackEvent`) ONLY — everything else
117
+ * keeps using `baseUrl`.
118
+ *
119
+ * Analytics must reach the Brainerce API **directly from the browser** so the
120
+ * visitor's real IP — and therefore country — is preserved. If a beacon is routed
121
+ * through a same-origin BFF proxy (see `proxyMode`), it arrives at the backend from
122
+ * the proxy server's datacenter IP instead (e.g. a Vercel US region), and every
123
+ * visitor is mis-geolocated to that datacenter.
124
+ *
125
+ * You normally do NOT need to set this: when `proxyMode` is on (or `baseUrl` shares
126
+ * the current page's origin), the SDK automatically sends analytics to
127
+ * `https://api.brainerce.com`. Set it explicitly only when your Brainerce API lives
128
+ * at a custom / self-hosted origin.
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * const client = new BrainerceClient({
133
+ * connectionId: 'vc_abc123...',
134
+ * baseUrl: '/api/store', // products / cart / auth go through the BFF proxy
135
+ * proxyMode: true,
136
+ * // analytics auto-routes direct to https://api.brainerce.com — no config needed
137
+ * });
138
+ * ```
139
+ */
140
+ analyticsBaseUrl?: string;
115
141
  }
116
142
  interface StoreInfo {
117
143
  id: string;
@@ -256,13 +282,20 @@ interface Product {
256
282
  */
257
283
  localeSlugs?: Record<string, string> | null;
258
284
  /**
259
- * Product description as HTML. **Always sanitize before rendering**, e.g.:
285
+ * Product description as HTML. May contain self-hosted `<video>` and
286
+ * host-locked YouTube/Vimeo `<iframe>` embeds. **Always sanitize before
287
+ * rendering**, e.g.:
260
288
  *
261
289
  * import { sanitizeHTML } from '@brainerce/utils';
262
290
  * <div dangerouslySetInnerHTML={{ __html: sanitizeHTML(product.description) }} />
263
291
  *
264
- * The backend strips dangerous tags/attributes on write, but storefronts should
265
- * still treat product description as untrusted HTML and sanitize on render.
292
+ * The backend strips dangerous tags/attributes on write, but storefronts
293
+ * should still treat this as untrusted HTML and sanitize on render. Your
294
+ * sanitizer must allow `video`/`source` and — for embeds — `iframe` restricted
295
+ * to the hosts `www.youtube.com`, `www.youtube-nocookie.com`,
296
+ * `player.vimeo.com` (never arbitrary iframes). Sizing is on the `width`/
297
+ * `height` attributes (inline `style` is stripped). If you set a CSP, add
298
+ * those three hosts to `frame-src` or embeds render blank.
266
299
  */
267
300
  description?: string | null;
268
301
  descriptionFormat?: 'text' | 'html' | 'markdown' | null;
@@ -1064,7 +1097,14 @@ interface CreateProductDto {
1064
1097
  status?: 'active' | 'draft';
1065
1098
  type?: 'SIMPLE' | 'VARIABLE';
1066
1099
  isDownloadable?: boolean;
1100
+ /** Existing category IDs to assign. Unknown/cross-store IDs are rejected with 400. To assign by name (auto-creating if missing), use `categoryNames`. */
1067
1101
  categories?: string[];
1102
+ /** Category names to assign — matched case-insensitively and auto-created if none exists. Merged with any IDs in `categories`. */
1103
+ categoryNames?: string[];
1104
+ /** Existing brand IDs to assign. Unknown/cross-store IDs are rejected with 400. To assign by name (auto-creating if missing), use `brandNames`. */
1105
+ brands?: string[];
1106
+ /** Brand names to assign — matched case-insensitively and auto-created if none exists. Merged with any IDs in `brands`. */
1107
+ brandNames?: string[];
1068
1108
  tags?: string[];
1069
1109
  images?: ProductImage[];
1070
1110
  }
@@ -1078,7 +1118,14 @@ interface UpdateProductDto {
1078
1118
  costPrice?: number | null;
1079
1119
  status?: 'active' | 'draft';
1080
1120
  isDownloadable?: boolean;
1121
+ /** Existing category IDs to assign (replaces the current set). Pass `[]` to clear. Unknown/cross-store IDs are rejected with 400. To assign by name (auto-creating if missing), use `categoryNames`. */
1081
1122
  categories?: string[];
1123
+ /** Category names to assign — matched case-insensitively and auto-created if missing. Additive: added to `categories` (or to the product's current categories when `categories` is omitted). */
1124
+ categoryNames?: string[];
1125
+ /** Existing brand IDs to assign (replaces the current set). Pass `[]` to clear. Unknown/cross-store IDs are rejected with 400. To assign by name (auto-creating if missing), use `brandNames`. */
1126
+ brands?: string[];
1127
+ /** Brand names to assign — matched case-insensitively and auto-created if missing. Additive: added to `brands` (or to the product's current brands when `brands` is omitted). */
1128
+ brandNames?: string[];
1082
1129
  tags?: string[];
1083
1130
  images?: ProductImage[];
1084
1131
  }
@@ -1806,6 +1853,20 @@ interface CartItem {
1806
1853
  notes?: string | null;
1807
1854
  /** Optional metadata for this line item */
1808
1855
  metadata?: Record<string, unknown> | null;
1856
+ /**
1857
+ * Resolved customer input field values with merchant-defined labels.
1858
+ * Mirrors `OrderItem.customizations` — same shape, live (not order-frozen).
1859
+ * Present only when the item has at least one customization value set.
1860
+ *
1861
+ * @example
1862
+ * // Display "Color: Silver / Size: 45" without an extra getProduct() call:
1863
+ * Object.entries(item.customizations ?? {}).map(([, c]) => `${c.label}: ${c.value}`)
1864
+ */
1865
+ customizations?: Record<string, {
1866
+ label: string;
1867
+ value: string | string[];
1868
+ type: string;
1869
+ }>;
1809
1870
  /**
1810
1871
  * Per-line modifier breakdown — present when the line was added with
1811
1872
  * modifier `selections`. Snapshot taken at add-time; immutable to catalog
@@ -2326,6 +2387,20 @@ interface CheckoutLineItem {
2326
2387
  notes?: string | null;
2327
2388
  /** Custom metadata including customer input field values */
2328
2389
  metadata?: Record<string, unknown> | null;
2390
+ /**
2391
+ * Resolved customer input field values with merchant-defined labels.
2392
+ * Mirrors `OrderItem.customizations` — same shape, live (not order-frozen).
2393
+ * Present only when the item has at least one customization value set.
2394
+ *
2395
+ * @example
2396
+ * // Render "Color: Silver / Ring size: 45" in your checkout summary:
2397
+ * Object.entries(item.customizations ?? {}).map(([, c]) => `${c.label}: ${c.value}`)
2398
+ */
2399
+ customizations?: Record<string, {
2400
+ label: string;
2401
+ value: string | string[];
2402
+ type: string;
2403
+ }>;
2329
2404
  }
2330
2405
  /**
2331
2406
  * Represents an available shipping rate/method for checkout.
@@ -3070,6 +3145,18 @@ interface PaymentClientSdk {
3070
3145
  *
3071
3146
  * **Alias:** `PaymentProvider` is the same type (use either name).
3072
3147
  */
3148
+ /**
3149
+ * Payment-method taxonomy (Shopify-parity). `CREDIT_CARD` is a *primary* card
3150
+ * processor — the single method that settles the order; everything else is an
3151
+ * *additive* method (wallet/alt/local/redeemable) that coexists alongside it.
3152
+ */
3153
+ type PaymentMethodType = 'CREDIT_CARD' | 'WALLET' | 'OFFSITE' | 'LOCAL' | 'REDEEMABLE';
3154
+ /**
3155
+ * How a storefront should render a payment method. `card_form` = the main card
3156
+ * fields; `express_button` = an accelerated-checkout button (render it ABOVE the
3157
+ * card form, like PayPal / Apple Pay); `redirect` = send the buyer offsite.
3158
+ */
3159
+ type PaymentPresentation = 'card_form' | 'express_button' | 'redirect' | 'local_option' | 'redeemable_field';
3073
3160
  interface PaymentProviderConfig {
3074
3161
  /** Provider ID (use this when creating payment intents) */
3075
3162
  id: string;
@@ -3085,8 +3172,26 @@ interface PaymentProviderConfig {
3085
3172
  supportedMethods: string[];
3086
3173
  /** Whether this is test/sandbox mode */
3087
3174
  testMode: boolean;
3088
- /** Whether this is the default provider */
3175
+ /**
3176
+ * Whether this is the store's default *settling* provider. Only a primary
3177
+ * (`methodType: 'CREDIT_CARD'`) provider is ever the default — an additive
3178
+ * wallet never becomes the default while a card processor is installed.
3179
+ */
3089
3180
  isDefault: boolean;
3181
+ /**
3182
+ * Payment-method taxonomy — primary card processor vs additive wallet/alt.
3183
+ * Use it to place the method: primary → the main card form; additive → an
3184
+ * express button. See `presentation`.
3185
+ */
3186
+ methodType: PaymentMethodType;
3187
+ /** Rendering hint derived from `methodType` (card_form | express_button | …). */
3188
+ presentation: PaymentPresentation;
3189
+ /**
3190
+ * True for wallet/alternative methods (e.g. PayPal) that are shown *alongside*
3191
+ * the primary card processor rather than replacing it. Render these as
3192
+ * accelerated-checkout express buttons above the card form.
3193
+ */
3194
+ isAdditive: boolean;
3090
3195
  /** Client-side SDK configuration for providers that render a widget */
3091
3196
  clientSdk?: PaymentClientSdk;
3092
3197
  }
@@ -3983,7 +4088,8 @@ interface MetafieldDefinition {
3983
4088
  maxLength?: number | null;
3984
4089
  minValue?: number | null;
3985
4090
  maxValue?: number | null;
3986
- enumValues?: string[] | null;
4091
+ /** Allowed values for SELECT / MULTI_SELECT fields, with optional swatch metadata. */
4092
+ enumValues?: CustomizationFieldOption[] | null;
3987
4093
  defaultValue?: string | null;
3988
4094
  position: number;
3989
4095
  isActive: boolean;
@@ -4067,7 +4173,8 @@ interface PublicMetafieldDefinition {
4067
4173
  * Pair with `getProducts({ metafields: { [key]: [...] } })`.
4068
4174
  */
4069
4175
  filterable?: boolean;
4070
- enumValues?: string[];
4176
+ /** Allowed values for SELECT / MULTI_SELECT fields, with optional swatch metadata. */
4177
+ enumValues?: CustomizationFieldOption[];
4071
4178
  minLength?: number | null;
4072
4179
  maxLength?: number | null;
4073
4180
  minValue?: number | null;
@@ -4075,6 +4182,20 @@ interface PublicMetafieldDefinition {
4075
4182
  defaultValue?: string | null;
4076
4183
  position: number;
4077
4184
  }
4185
+ /**
4186
+ * A single selectable option for a SELECT or MULTI_SELECT customization field.
4187
+ * Legacy string-only `enumValues` arrays are automatically promoted to this shape.
4188
+ */
4189
+ interface CustomizationFieldOption {
4190
+ /** Human-readable label shown in the storefront UI (e.g., "Rose Gold"). */
4191
+ label: string;
4192
+ /** Value stored in `CartItem.metadata` and carried through to `OrderItem.customizations`. */
4193
+ value: string;
4194
+ /** Optional CSS hex color for rendering a color swatch (e.g., "#B76E79"). */
4195
+ swatchColor?: string | null;
4196
+ /** Optional image URL for rendering a texture/image swatch. */
4197
+ swatchImageUrl?: string | null;
4198
+ }
4078
4199
  /**
4079
4200
  * Customer-facing input field definition assigned to a specific product.
4080
4201
  * Returned in product responses to tell the storefront which input fields to render.
@@ -4090,7 +4211,11 @@ interface ProductCustomizationField {
4090
4211
  maxLength?: number | null;
4091
4212
  minValue?: number | null;
4092
4213
  maxValue?: number | null;
4093
- enumValues?: string[];
4214
+ /**
4215
+ * Allowed values for SELECT / MULTI_SELECT fields.
4216
+ * Each option includes a label, value, and optional swatch color/image.
4217
+ */
4218
+ enumValues?: CustomizationFieldOption[];
4094
4219
  defaultValue?: string | null;
4095
4220
  position: number;
4096
4221
  }
@@ -5379,6 +5504,8 @@ declare class BrainerceClient {
5379
5504
  private readonly storeId?;
5380
5505
  private readonly connectionId?;
5381
5506
  private readonly baseUrl;
5507
+ /** Direct API base for analytics beacons — bypasses any same-origin BFF proxy so the visitor's real IP (hence country) reaches the backend. */
5508
+ private readonly analyticsBaseUrl;
5382
5509
  private readonly timeout;
5383
5510
  private customerToken;
5384
5511
  private customerCartId;
@@ -5548,6 +5675,20 @@ declare class BrainerceClient {
5548
5675
  * ```
5549
5676
  */
5550
5677
  trackEvent(payload?: TrackEventPayload): Promise<void>;
5678
+ /**
5679
+ * Resolve the base URL for analytics beacons. Analytics must go DIRECTLY to the
5680
+ * Brainerce API (never through a same-origin BFF proxy) so the visitor's real IP —
5681
+ * hence country — reaches the backend. Explicit `analyticsBaseUrl` wins; otherwise
5682
+ * use the direct API when `baseUrl` is a same-origin proxy, else the (already-direct)
5683
+ * `baseUrl` (which may be a custom / self-hosted Brainerce API).
5684
+ */
5685
+ private resolveAnalyticsBaseUrl;
5686
+ /**
5687
+ * Fire-and-forget analytics beacon sent directly to `analyticsBaseUrl`. Uses
5688
+ * `keepalive` so it survives page unload (engagement events fired on pagehide),
5689
+ * omits credentials (cookieless), and never throws.
5690
+ */
5691
+ private sendAnalyticsBeacon;
5551
5692
  /**
5552
5693
  * Get a list of products with pagination and filtering
5553
5694
  * Works in vibe-coded, storefront (public), and admin mode
@@ -5972,6 +6113,29 @@ declare class BrainerceClient {
5972
6113
  * ```
5973
6114
  */
5974
6115
  updateOrderShipping(orderId: string, data: UpdateOrderShippingDto): Promise<Order>;
6116
+ /**
6117
+ * Create a shipping label for an order via the installed App Store shipping app.
6118
+ * Pass the rate ID returned from checkout rate shopping. Billing goes directly
6119
+ * to the merchant's carrier account — Brainerce is not a billing intermediary.
6120
+ *
6121
+ * @example
6122
+ * ```typescript
6123
+ * const label = await client.createShippingLabel('order_abc', {
6124
+ * rateId: 'rate_8f123456789abcdef',
6125
+ * });
6126
+ * console.log('Label URL:', label.labelUrl);
6127
+ * console.log('Tracking:', label.trackingNumber);
6128
+ * ```
6129
+ */
6130
+ createShippingLabel(orderId: string, data: {
6131
+ rateId: string;
6132
+ parcel?: Record<string, string>;
6133
+ }): Promise<{
6134
+ shipmentId: string;
6135
+ labelUrl: string;
6136
+ trackingNumber: string;
6137
+ carrier: string;
6138
+ }>;
5975
6139
  /**
5976
6140
  * Cancel an order
5977
6141
  * Works for Shopify and WooCommerce orders that haven't been fulfilled
@@ -7859,6 +8023,14 @@ declare class BrainerceClient {
7859
8023
  *
7860
8024
  * @returns Payment providers config with hasPayments flag and provider list
7861
8025
  *
8026
+ * Each provider carries a `methodType` (Shopify-parity taxonomy) and a derived
8027
+ * `isAdditive` / `presentation`. There is at most ONE primary card processor
8028
+ * (`methodType: 'CREDIT_CARD'`, the `defaultProvider`) that settles the order;
8029
+ * additive methods (`isAdditive: true`, e.g. PayPal as a `'WALLET'`) are shown
8030
+ * *alongside* it as accelerated-checkout express buttons — they never replace
8031
+ * the card form. A wallet-only store is the one exception: with no card
8032
+ * processor installed, the wallet becomes the `defaultProvider` and stands alone.
8033
+ *
7862
8034
  * @example
7863
8035
  * ```typescript
7864
8036
  * const { providers, hasPayments, defaultProvider } = await client.getPaymentProviders();
@@ -7868,27 +8040,21 @@ declare class BrainerceClient {
7868
8040
  * return;
7869
8041
  * }
7870
8042
  *
7871
- * // Find specific providers
7872
- * const stripeProvider = providers.find(p => p.provider === 'stripe');
7873
- * const paypalProvider = providers.find(p => p.provider === 'paypal');
7874
- *
7875
- * // Initialize Stripe if available (with Connect account support)
7876
- * if (stripeProvider) {
7877
- * const stripe = await loadStripe(stripeProvider.publicKey, {
7878
- * stripeAccount: stripeProvider.stripeAccountId, // For Stripe Connect
7879
- * });
7880
- * // Show Stripe payment form
7881
- * }
7882
- *
7883
- * // Initialize PayPal if available
7884
- * if (paypalProvider) {
7885
- * // Load PayPal SDK with paypalProvider.publicKey as client-id
7886
- * // Show PayPal buttons
8043
+ * // Render additive methods (wallets) as express buttons ABOVE the card form.
8044
+ * const expressMethods = providers.filter(p => p.isAdditive); // e.g. PayPal
8045
+ * for (const m of expressMethods) {
8046
+ * // m.presentation === 'express_button' — render an express/accelerated button
8047
+ * // and call createPaymentIntent({ providerId: m.id }) when the buyer taps it.
7887
8048
  * }
7888
8049
  *
7889
- * // Or use the default provider for a simpler single-provider flow
7890
- * if (defaultProvider) {
7891
- * console.log(`Using default: ${defaultProvider.name} (${defaultProvider.provider})`);
8050
+ * // Render the primary card processor (the settling default) as the main form.
8051
+ * if (defaultProvider && !defaultProvider.isAdditive) {
8052
+ * if (defaultProvider.provider === 'stripe') {
8053
+ * const stripe = await loadStripe(defaultProvider.publicKey, {
8054
+ * stripeAccount: defaultProvider.stripeAccountId, // For Stripe Connect
8055
+ * });
8056
+ * // Show the Stripe card form
8057
+ * }
7892
8058
  * }
7893
8059
  * ```
7894
8060
  */
@@ -9543,7 +9709,7 @@ declare class BrainerceError extends Error {
9543
9709
  constructor(message: string, statusCode: number, details?: unknown);
9544
9710
  }
9545
9711
 
9546
- declare const SDK_VERSION = "1.36.1";
9712
+ declare const SDK_VERSION = "1.41.0";
9547
9713
 
9548
9714
  /**
9549
9715
  * Verify a webhook signature from Brainerce
package/dist/index.js CHANGED
@@ -185,7 +185,7 @@ function isDevGuardsEnabled() {
185
185
  }
186
186
 
187
187
  // src/version.ts
188
- var SDK_VERSION = "1.36.1";
188
+ var SDK_VERSION = "1.41.0";
189
189
 
190
190
  // src/client.ts
191
191
  var DEFAULT_BASE_URL = "https://api.brainerce.com";
@@ -653,6 +653,7 @@ var BrainerceClient = class {
653
653
  }
654
654
  }
655
655
  this.proxyMode = options.proxyMode || false;
656
+ this.analyticsBaseUrl = this.resolveAnalyticsBaseUrl(options.analyticsBaseUrl, resolvedBase);
656
657
  this.onAuthError = options.onAuthError;
657
658
  this.hydrateSessionCart();
658
659
  this.detectRecoverCartFromUrl();
@@ -1106,12 +1107,46 @@ var BrainerceClient = class {
1106
1107
  * ```
1107
1108
  */
1108
1109
  async trackEvent(payload = {}) {
1110
+ if (this.isVibeCodedMode() && this.connectionId) {
1111
+ this.sendAnalyticsBeacon(`/api/vc/${encodePathSegment(this.connectionId)}/events`, payload);
1112
+ } else if (this.isStorefrontMode() && this.storeId) {
1113
+ this.sendAnalyticsBeacon(`/api/stores/${encodePathSegment(this.storeId)}/events`, payload);
1114
+ }
1115
+ }
1116
+ /**
1117
+ * Resolve the base URL for analytics beacons. Analytics must go DIRECTLY to the
1118
+ * Brainerce API (never through a same-origin BFF proxy) so the visitor's real IP —
1119
+ * hence country — reaches the backend. Explicit `analyticsBaseUrl` wins; otherwise
1120
+ * use the direct API when `baseUrl` is a same-origin proxy, else the (already-direct)
1121
+ * `baseUrl` (which may be a custom / self-hosted Brainerce API).
1122
+ */
1123
+ resolveAnalyticsBaseUrl(explicit, resolvedBase) {
1124
+ if (explicit) return explicit.replace(/\/$/, "");
1125
+ const sameOriginProxy = this.proxyMode || typeof window !== "undefined" && !!window.location?.origin && resolvedBase.startsWith(window.location.origin);
1126
+ return sameOriginProxy ? DEFAULT_BASE_URL : resolvedBase;
1127
+ }
1128
+ /**
1129
+ * Fire-and-forget analytics beacon sent directly to `analyticsBaseUrl`. Uses
1130
+ * `keepalive` so it survives page unload (engagement events fired on pagehide),
1131
+ * omits credentials (cookieless), and never throws.
1132
+ */
1133
+ sendAnalyticsBeacon(eventsPath, payload) {
1109
1134
  try {
1110
- if (this.isVibeCodedMode()) {
1111
- await this.vibeCodedRequest("POST", "/events", payload);
1112
- } else if (this.isStorefrontMode()) {
1113
- await this.storefrontRequest("POST", "/events", payload);
1114
- }
1135
+ const headers = {
1136
+ "Content-Type": "application/json",
1137
+ "X-SDK-Version": SDK_VERSION,
1138
+ "ngrok-skip-browser-warning": "true"
1139
+ };
1140
+ if (this.origin) headers["Origin"] = this.origin;
1141
+ void fetch(`${this.analyticsBaseUrl}${eventsPath}`, {
1142
+ method: "POST",
1143
+ headers,
1144
+ body: JSON.stringify(payload),
1145
+ keepalive: true,
1146
+ credentials: "omit",
1147
+ mode: "cors"
1148
+ }).catch(() => {
1149
+ });
1115
1150
  } catch {
1116
1151
  }
1117
1152
  }
@@ -1805,6 +1840,27 @@ var BrainerceClient = class {
1805
1840
  data
1806
1841
  );
1807
1842
  }
1843
+ /**
1844
+ * Create a shipping label for an order via the installed App Store shipping app.
1845
+ * Pass the rate ID returned from checkout rate shopping. Billing goes directly
1846
+ * to the merchant's carrier account — Brainerce is not a billing intermediary.
1847
+ *
1848
+ * @example
1849
+ * ```typescript
1850
+ * const label = await client.createShippingLabel('order_abc', {
1851
+ * rateId: 'rate_8f123456789abcdef',
1852
+ * });
1853
+ * console.log('Label URL:', label.labelUrl);
1854
+ * console.log('Tracking:', label.trackingNumber);
1855
+ * ```
1856
+ */
1857
+ async createShippingLabel(orderId, data) {
1858
+ return this.request(
1859
+ "POST",
1860
+ `/api/v1/orders/${encodePathSegment(orderId)}/shipments/app-label`,
1861
+ data
1862
+ );
1863
+ }
1808
1864
  /**
1809
1865
  * Cancel an order
1810
1866
  * Works for Shopify and WooCommerce orders that haven't been fulfilled
@@ -5208,6 +5264,14 @@ var BrainerceClient = class {
5208
5264
  *
5209
5265
  * @returns Payment providers config with hasPayments flag and provider list
5210
5266
  *
5267
+ * Each provider carries a `methodType` (Shopify-parity taxonomy) and a derived
5268
+ * `isAdditive` / `presentation`. There is at most ONE primary card processor
5269
+ * (`methodType: 'CREDIT_CARD'`, the `defaultProvider`) that settles the order;
5270
+ * additive methods (`isAdditive: true`, e.g. PayPal as a `'WALLET'`) are shown
5271
+ * *alongside* it as accelerated-checkout express buttons — they never replace
5272
+ * the card form. A wallet-only store is the one exception: with no card
5273
+ * processor installed, the wallet becomes the `defaultProvider` and stands alone.
5274
+ *
5211
5275
  * @example
5212
5276
  * ```typescript
5213
5277
  * const { providers, hasPayments, defaultProvider } = await client.getPaymentProviders();
@@ -5217,27 +5281,21 @@ var BrainerceClient = class {
5217
5281
  * return;
5218
5282
  * }
5219
5283
  *
5220
- * // Find specific providers
5221
- * const stripeProvider = providers.find(p => p.provider === 'stripe');
5222
- * const paypalProvider = providers.find(p => p.provider === 'paypal');
5223
- *
5224
- * // Initialize Stripe if available (with Connect account support)
5225
- * if (stripeProvider) {
5226
- * const stripe = await loadStripe(stripeProvider.publicKey, {
5227
- * stripeAccount: stripeProvider.stripeAccountId, // For Stripe Connect
5228
- * });
5229
- * // Show Stripe payment form
5230
- * }
5231
- *
5232
- * // Initialize PayPal if available
5233
- * if (paypalProvider) {
5234
- * // Load PayPal SDK with paypalProvider.publicKey as client-id
5235
- * // Show PayPal buttons
5284
+ * // Render additive methods (wallets) as express buttons ABOVE the card form.
5285
+ * const expressMethods = providers.filter(p => p.isAdditive); // e.g. PayPal
5286
+ * for (const m of expressMethods) {
5287
+ * // m.presentation === 'express_button' — render an express/accelerated button
5288
+ * // and call createPaymentIntent({ providerId: m.id }) when the buyer taps it.
5236
5289
  * }
5237
5290
  *
5238
- * // Or use the default provider for a simpler single-provider flow
5239
- * if (defaultProvider) {
5240
- * console.log(`Using default: ${defaultProvider.name} (${defaultProvider.provider})`);
5291
+ * // Render the primary card processor (the settling default) as the main form.
5292
+ * if (defaultProvider && !defaultProvider.isAdditive) {
5293
+ * if (defaultProvider.provider === 'stripe') {
5294
+ * const stripe = await loadStripe(defaultProvider.publicKey, {
5295
+ * stripeAccount: defaultProvider.stripeAccountId, // For Stripe Connect
5296
+ * });
5297
+ * // Show the Stripe card form
5298
+ * }
5241
5299
  * }
5242
5300
  * ```
5243
5301
  */
package/dist/index.mjs CHANGED
@@ -115,7 +115,7 @@ function isDevGuardsEnabled() {
115
115
  }
116
116
 
117
117
  // src/version.ts
118
- var SDK_VERSION = "1.36.1";
118
+ var SDK_VERSION = "1.41.0";
119
119
 
120
120
  // src/client.ts
121
121
  var DEFAULT_BASE_URL = "https://api.brainerce.com";
@@ -583,6 +583,7 @@ var BrainerceClient = class {
583
583
  }
584
584
  }
585
585
  this.proxyMode = options.proxyMode || false;
586
+ this.analyticsBaseUrl = this.resolveAnalyticsBaseUrl(options.analyticsBaseUrl, resolvedBase);
586
587
  this.onAuthError = options.onAuthError;
587
588
  this.hydrateSessionCart();
588
589
  this.detectRecoverCartFromUrl();
@@ -1036,12 +1037,46 @@ var BrainerceClient = class {
1036
1037
  * ```
1037
1038
  */
1038
1039
  async trackEvent(payload = {}) {
1040
+ if (this.isVibeCodedMode() && this.connectionId) {
1041
+ this.sendAnalyticsBeacon(`/api/vc/${encodePathSegment(this.connectionId)}/events`, payload);
1042
+ } else if (this.isStorefrontMode() && this.storeId) {
1043
+ this.sendAnalyticsBeacon(`/api/stores/${encodePathSegment(this.storeId)}/events`, payload);
1044
+ }
1045
+ }
1046
+ /**
1047
+ * Resolve the base URL for analytics beacons. Analytics must go DIRECTLY to the
1048
+ * Brainerce API (never through a same-origin BFF proxy) so the visitor's real IP —
1049
+ * hence country — reaches the backend. Explicit `analyticsBaseUrl` wins; otherwise
1050
+ * use the direct API when `baseUrl` is a same-origin proxy, else the (already-direct)
1051
+ * `baseUrl` (which may be a custom / self-hosted Brainerce API).
1052
+ */
1053
+ resolveAnalyticsBaseUrl(explicit, resolvedBase) {
1054
+ if (explicit) return explicit.replace(/\/$/, "");
1055
+ const sameOriginProxy = this.proxyMode || typeof window !== "undefined" && !!window.location?.origin && resolvedBase.startsWith(window.location.origin);
1056
+ return sameOriginProxy ? DEFAULT_BASE_URL : resolvedBase;
1057
+ }
1058
+ /**
1059
+ * Fire-and-forget analytics beacon sent directly to `analyticsBaseUrl`. Uses
1060
+ * `keepalive` so it survives page unload (engagement events fired on pagehide),
1061
+ * omits credentials (cookieless), and never throws.
1062
+ */
1063
+ sendAnalyticsBeacon(eventsPath, payload) {
1039
1064
  try {
1040
- if (this.isVibeCodedMode()) {
1041
- await this.vibeCodedRequest("POST", "/events", payload);
1042
- } else if (this.isStorefrontMode()) {
1043
- await this.storefrontRequest("POST", "/events", payload);
1044
- }
1065
+ const headers = {
1066
+ "Content-Type": "application/json",
1067
+ "X-SDK-Version": SDK_VERSION,
1068
+ "ngrok-skip-browser-warning": "true"
1069
+ };
1070
+ if (this.origin) headers["Origin"] = this.origin;
1071
+ void fetch(`${this.analyticsBaseUrl}${eventsPath}`, {
1072
+ method: "POST",
1073
+ headers,
1074
+ body: JSON.stringify(payload),
1075
+ keepalive: true,
1076
+ credentials: "omit",
1077
+ mode: "cors"
1078
+ }).catch(() => {
1079
+ });
1045
1080
  } catch {
1046
1081
  }
1047
1082
  }
@@ -1735,6 +1770,27 @@ var BrainerceClient = class {
1735
1770
  data
1736
1771
  );
1737
1772
  }
1773
+ /**
1774
+ * Create a shipping label for an order via the installed App Store shipping app.
1775
+ * Pass the rate ID returned from checkout rate shopping. Billing goes directly
1776
+ * to the merchant's carrier account — Brainerce is not a billing intermediary.
1777
+ *
1778
+ * @example
1779
+ * ```typescript
1780
+ * const label = await client.createShippingLabel('order_abc', {
1781
+ * rateId: 'rate_8f123456789abcdef',
1782
+ * });
1783
+ * console.log('Label URL:', label.labelUrl);
1784
+ * console.log('Tracking:', label.trackingNumber);
1785
+ * ```
1786
+ */
1787
+ async createShippingLabel(orderId, data) {
1788
+ return this.request(
1789
+ "POST",
1790
+ `/api/v1/orders/${encodePathSegment(orderId)}/shipments/app-label`,
1791
+ data
1792
+ );
1793
+ }
1738
1794
  /**
1739
1795
  * Cancel an order
1740
1796
  * Works for Shopify and WooCommerce orders that haven't been fulfilled
@@ -5138,6 +5194,14 @@ var BrainerceClient = class {
5138
5194
  *
5139
5195
  * @returns Payment providers config with hasPayments flag and provider list
5140
5196
  *
5197
+ * Each provider carries a `methodType` (Shopify-parity taxonomy) and a derived
5198
+ * `isAdditive` / `presentation`. There is at most ONE primary card processor
5199
+ * (`methodType: 'CREDIT_CARD'`, the `defaultProvider`) that settles the order;
5200
+ * additive methods (`isAdditive: true`, e.g. PayPal as a `'WALLET'`) are shown
5201
+ * *alongside* it as accelerated-checkout express buttons — they never replace
5202
+ * the card form. A wallet-only store is the one exception: with no card
5203
+ * processor installed, the wallet becomes the `defaultProvider` and stands alone.
5204
+ *
5141
5205
  * @example
5142
5206
  * ```typescript
5143
5207
  * const { providers, hasPayments, defaultProvider } = await client.getPaymentProviders();
@@ -5147,27 +5211,21 @@ var BrainerceClient = class {
5147
5211
  * return;
5148
5212
  * }
5149
5213
  *
5150
- * // Find specific providers
5151
- * const stripeProvider = providers.find(p => p.provider === 'stripe');
5152
- * const paypalProvider = providers.find(p => p.provider === 'paypal');
5153
- *
5154
- * // Initialize Stripe if available (with Connect account support)
5155
- * if (stripeProvider) {
5156
- * const stripe = await loadStripe(stripeProvider.publicKey, {
5157
- * stripeAccount: stripeProvider.stripeAccountId, // For Stripe Connect
5158
- * });
5159
- * // Show Stripe payment form
5160
- * }
5161
- *
5162
- * // Initialize PayPal if available
5163
- * if (paypalProvider) {
5164
- * // Load PayPal SDK with paypalProvider.publicKey as client-id
5165
- * // Show PayPal buttons
5214
+ * // Render additive methods (wallets) as express buttons ABOVE the card form.
5215
+ * const expressMethods = providers.filter(p => p.isAdditive); // e.g. PayPal
5216
+ * for (const m of expressMethods) {
5217
+ * // m.presentation === 'express_button' — render an express/accelerated button
5218
+ * // and call createPaymentIntent({ providerId: m.id }) when the buyer taps it.
5166
5219
  * }
5167
5220
  *
5168
- * // Or use the default provider for a simpler single-provider flow
5169
- * if (defaultProvider) {
5170
- * console.log(`Using default: ${defaultProvider.name} (${defaultProvider.provider})`);
5221
+ * // Render the primary card processor (the settling default) as the main form.
5222
+ * if (defaultProvider && !defaultProvider.isAdditive) {
5223
+ * if (defaultProvider.provider === 'stripe') {
5224
+ * const stripe = await loadStripe(defaultProvider.publicKey, {
5225
+ * stripeAccount: defaultProvider.stripeAccountId, // For Stripe Connect
5226
+ * });
5227
+ * // Show the Stripe card form
5228
+ * }
5171
5229
  * }
5172
5230
  * ```
5173
5231
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainerce",
3
- "version": "1.38.0",
3
+ "version": "1.41.0",
4
4
  "description": "Official SDK for building e-commerce storefronts with Brainerce Platform. Perfect for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",