brainerce 1.39.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';
@@ -2375,7 +2375,16 @@ const checkoutId = checkout.id;
2375
2375
 
2376
2376
  #### Get All Payment Providers (Recommended)
2377
2377
 
2378
- 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.)
2379
2388
 
2380
2389
  ```typescript
2381
2390
  const { hasPayments, providers, defaultProvider } = await client.getPaymentProviders();
@@ -2391,7 +2400,10 @@ const { hasPayments, providers, defaultProvider } = await client.getPaymentProvi
2391
2400
  // publicKey: 'pk_live_xxx...',
2392
2401
  // supportedMethods: ['card', 'ideal'],
2393
2402
  // testMode: false,
2394
- // isDefault: true
2403
+ // isDefault: true,
2404
+ // methodType: 'CREDIT_CARD', // primary card processor
2405
+ // presentation: 'card_form',
2406
+ // isAdditive: false
2395
2407
  // },
2396
2408
  // {
2397
2409
  // id: 'provider_yyy',
@@ -2400,12 +2412,20 @@ const { hasPayments, providers, defaultProvider } = await client.getPaymentProvi
2400
2412
  // publicKey: 'client_id_xxx...',
2401
2413
  // supportedMethods: ['paypal'],
2402
2414
  // testMode: false,
2403
- // isDefault: false
2415
+ // isDefault: false,
2416
+ // methodType: 'WALLET', // additive — render as an express button
2417
+ // presentation: 'express_button',
2418
+ // isAdditive: true
2404
2419
  // }
2405
2420
  // ],
2406
- // defaultProvider: { ... } // The default provider (first one)
2421
+ // defaultProvider: { ... } // The settling primary (never an additive wallet
2422
+ // // while a card processor is installed)
2407
2423
  // }
2408
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
+
2409
2429
  // Build dynamic UI based on available providers
2410
2430
  if (!hasPayments) {
2411
2431
  return <div>Payment not configured for this store</div>;
@@ -3424,6 +3444,8 @@ client.trackEvent({ eventType: 'engagement', path: '/products/shoes', engagedMs:
3424
3444
 
3425
3445
  > `trackEvent()` is fire-and-forget — errors are silently swallowed so a failed beacon never breaks your storefront. Available in `salesChannelId` and `storeId` modes.
3426
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
+
3427
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.
3428
3450
 
3429
3451
  ---
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
  }
@@ -3098,6 +3145,18 @@ interface PaymentClientSdk {
3098
3145
  *
3099
3146
  * **Alias:** `PaymentProvider` is the same type (use either name).
3100
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';
3101
3160
  interface PaymentProviderConfig {
3102
3161
  /** Provider ID (use this when creating payment intents) */
3103
3162
  id: string;
@@ -3113,8 +3172,26 @@ interface PaymentProviderConfig {
3113
3172
  supportedMethods: string[];
3114
3173
  /** Whether this is test/sandbox mode */
3115
3174
  testMode: boolean;
3116
- /** 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
+ */
3117
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;
3118
3195
  /** Client-side SDK configuration for providers that render a widget */
3119
3196
  clientSdk?: PaymentClientSdk;
3120
3197
  }
@@ -5427,6 +5504,8 @@ declare class BrainerceClient {
5427
5504
  private readonly storeId?;
5428
5505
  private readonly connectionId?;
5429
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;
5430
5509
  private readonly timeout;
5431
5510
  private customerToken;
5432
5511
  private customerCartId;
@@ -5596,6 +5675,20 @@ declare class BrainerceClient {
5596
5675
  * ```
5597
5676
  */
5598
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;
5599
5692
  /**
5600
5693
  * Get a list of products with pagination and filtering
5601
5694
  * Works in vibe-coded, storefront (public), and admin mode
@@ -7930,6 +8023,14 @@ declare class BrainerceClient {
7930
8023
  *
7931
8024
  * @returns Payment providers config with hasPayments flag and provider list
7932
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
+ *
7933
8034
  * @example
7934
8035
  * ```typescript
7935
8036
  * const { providers, hasPayments, defaultProvider } = await client.getPaymentProviders();
@@ -7939,27 +8040,21 @@ declare class BrainerceClient {
7939
8040
  * return;
7940
8041
  * }
7941
8042
  *
7942
- * // Find specific providers
7943
- * const stripeProvider = providers.find(p => p.provider === 'stripe');
7944
- * const paypalProvider = providers.find(p => p.provider === 'paypal');
7945
- *
7946
- * // Initialize Stripe if available (with Connect account support)
7947
- * if (stripeProvider) {
7948
- * const stripe = await loadStripe(stripeProvider.publicKey, {
7949
- * stripeAccount: stripeProvider.stripeAccountId, // For Stripe Connect
7950
- * });
7951
- * // Show Stripe payment form
7952
- * }
7953
- *
7954
- * // Initialize PayPal if available
7955
- * if (paypalProvider) {
7956
- * // Load PayPal SDK with paypalProvider.publicKey as client-id
7957
- * // 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.
7958
8048
  * }
7959
8049
  *
7960
- * // Or use the default provider for a simpler single-provider flow
7961
- * if (defaultProvider) {
7962
- * 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
+ * }
7963
8058
  * }
7964
8059
  * ```
7965
8060
  */
@@ -9614,7 +9709,7 @@ declare class BrainerceError extends Error {
9614
9709
  constructor(message: string, statusCode: number, details?: unknown);
9615
9710
  }
9616
9711
 
9617
- declare const SDK_VERSION = "1.36.1";
9712
+ declare const SDK_VERSION = "1.41.0";
9618
9713
 
9619
9714
  /**
9620
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
  }
@@ -3098,6 +3145,18 @@ interface PaymentClientSdk {
3098
3145
  *
3099
3146
  * **Alias:** `PaymentProvider` is the same type (use either name).
3100
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';
3101
3160
  interface PaymentProviderConfig {
3102
3161
  /** Provider ID (use this when creating payment intents) */
3103
3162
  id: string;
@@ -3113,8 +3172,26 @@ interface PaymentProviderConfig {
3113
3172
  supportedMethods: string[];
3114
3173
  /** Whether this is test/sandbox mode */
3115
3174
  testMode: boolean;
3116
- /** 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
+ */
3117
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;
3118
3195
  /** Client-side SDK configuration for providers that render a widget */
3119
3196
  clientSdk?: PaymentClientSdk;
3120
3197
  }
@@ -5427,6 +5504,8 @@ declare class BrainerceClient {
5427
5504
  private readonly storeId?;
5428
5505
  private readonly connectionId?;
5429
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;
5430
5509
  private readonly timeout;
5431
5510
  private customerToken;
5432
5511
  private customerCartId;
@@ -5596,6 +5675,20 @@ declare class BrainerceClient {
5596
5675
  * ```
5597
5676
  */
5598
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;
5599
5692
  /**
5600
5693
  * Get a list of products with pagination and filtering
5601
5694
  * Works in vibe-coded, storefront (public), and admin mode
@@ -7930,6 +8023,14 @@ declare class BrainerceClient {
7930
8023
  *
7931
8024
  * @returns Payment providers config with hasPayments flag and provider list
7932
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
+ *
7933
8034
  * @example
7934
8035
  * ```typescript
7935
8036
  * const { providers, hasPayments, defaultProvider } = await client.getPaymentProviders();
@@ -7939,27 +8040,21 @@ declare class BrainerceClient {
7939
8040
  * return;
7940
8041
  * }
7941
8042
  *
7942
- * // Find specific providers
7943
- * const stripeProvider = providers.find(p => p.provider === 'stripe');
7944
- * const paypalProvider = providers.find(p => p.provider === 'paypal');
7945
- *
7946
- * // Initialize Stripe if available (with Connect account support)
7947
- * if (stripeProvider) {
7948
- * const stripe = await loadStripe(stripeProvider.publicKey, {
7949
- * stripeAccount: stripeProvider.stripeAccountId, // For Stripe Connect
7950
- * });
7951
- * // Show Stripe payment form
7952
- * }
7953
- *
7954
- * // Initialize PayPal if available
7955
- * if (paypalProvider) {
7956
- * // Load PayPal SDK with paypalProvider.publicKey as client-id
7957
- * // 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.
7958
8048
  * }
7959
8049
  *
7960
- * // Or use the default provider for a simpler single-provider flow
7961
- * if (defaultProvider) {
7962
- * 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
+ * }
7963
8058
  * }
7964
8059
  * ```
7965
8060
  */
@@ -9614,7 +9709,7 @@ declare class BrainerceError extends Error {
9614
9709
  constructor(message: string, statusCode: number, details?: unknown);
9615
9710
  }
9616
9711
 
9617
- declare const SDK_VERSION = "1.36.1";
9712
+ declare const SDK_VERSION = "1.41.0";
9618
9713
 
9619
9714
  /**
9620
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
  }
@@ -5229,6 +5264,14 @@ var BrainerceClient = class {
5229
5264
  *
5230
5265
  * @returns Payment providers config with hasPayments flag and provider list
5231
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
+ *
5232
5275
  * @example
5233
5276
  * ```typescript
5234
5277
  * const { providers, hasPayments, defaultProvider } = await client.getPaymentProviders();
@@ -5238,27 +5281,21 @@ var BrainerceClient = class {
5238
5281
  * return;
5239
5282
  * }
5240
5283
  *
5241
- * // Find specific providers
5242
- * const stripeProvider = providers.find(p => p.provider === 'stripe');
5243
- * const paypalProvider = providers.find(p => p.provider === 'paypal');
5244
- *
5245
- * // Initialize Stripe if available (with Connect account support)
5246
- * if (stripeProvider) {
5247
- * const stripe = await loadStripe(stripeProvider.publicKey, {
5248
- * stripeAccount: stripeProvider.stripeAccountId, // For Stripe Connect
5249
- * });
5250
- * // Show Stripe payment form
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.
5251
5289
  * }
5252
5290
  *
5253
- * // Initialize PayPal if available
5254
- * if (paypalProvider) {
5255
- * // Load PayPal SDK with paypalProvider.publicKey as client-id
5256
- * // Show PayPal buttons
5257
- * }
5258
- *
5259
- * // Or use the default provider for a simpler single-provider flow
5260
- * if (defaultProvider) {
5261
- * 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
+ * }
5262
5299
  * }
5263
5300
  * ```
5264
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
  }
@@ -5159,6 +5194,14 @@ var BrainerceClient = class {
5159
5194
  *
5160
5195
  * @returns Payment providers config with hasPayments flag and provider list
5161
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
+ *
5162
5205
  * @example
5163
5206
  * ```typescript
5164
5207
  * const { providers, hasPayments, defaultProvider } = await client.getPaymentProviders();
@@ -5168,27 +5211,21 @@ var BrainerceClient = class {
5168
5211
  * return;
5169
5212
  * }
5170
5213
  *
5171
- * // Find specific providers
5172
- * const stripeProvider = providers.find(p => p.provider === 'stripe');
5173
- * const paypalProvider = providers.find(p => p.provider === 'paypal');
5174
- *
5175
- * // Initialize Stripe if available (with Connect account support)
5176
- * if (stripeProvider) {
5177
- * const stripe = await loadStripe(stripeProvider.publicKey, {
5178
- * stripeAccount: stripeProvider.stripeAccountId, // For Stripe Connect
5179
- * });
5180
- * // Show Stripe payment form
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.
5181
5219
  * }
5182
5220
  *
5183
- * // Initialize PayPal if available
5184
- * if (paypalProvider) {
5185
- * // Load PayPal SDK with paypalProvider.publicKey as client-id
5186
- * // Show PayPal buttons
5187
- * }
5188
- *
5189
- * // Or use the default provider for a simpler single-provider flow
5190
- * if (defaultProvider) {
5191
- * 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
+ * }
5192
5229
  * }
5193
5230
  * ```
5194
5231
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainerce",
3
- "version": "1.39.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",