brainerce 1.53.1 → 1.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -12,7 +12,7 @@ var CART_GUARDS = [
12
12
  var CART_ITEM_GUARDS = [
13
13
  {
14
14
  property: "name",
15
- message: 'CartItem has no "name" field. Use item.product.name or getCartItemName(item).\nImport: import { getCartItemName } from "brainerce";'
15
+ message: 'CartItem has no "name" field. Use getCartItemName(item) for the full\n"Product - Variant" label, or item.product.name for the product name alone.\nImport: import { getCartItemName } from "brainerce";'
16
16
  },
17
17
  {
18
18
  property: "price",
@@ -115,11 +115,29 @@ function isDevGuardsEnabled() {
115
115
  }
116
116
 
117
117
  // src/version.ts
118
- var SDK_VERSION = "1.53.1";
118
+ var SDK_VERSION = "1.54.0";
119
119
 
120
120
  // src/client.ts
121
121
  var DEFAULT_BASE_URL = "https://api.brainerce.com";
122
122
  var DEFAULT_TIMEOUT = 3e4;
123
+ var META_EVENT_NAMES = {
124
+ view_item: "ViewContent",
125
+ add_to_cart: "AddToCart",
126
+ begin_checkout: "InitiateCheckout",
127
+ add_payment_info: "AddPaymentInfo",
128
+ purchase: "Purchase",
129
+ search: "Search",
130
+ sign_up: "CompleteRegistration"
131
+ };
132
+ var TIKTOK_EVENT_NAMES = {
133
+ view_item: "ViewContent",
134
+ add_to_cart: "AddToCart",
135
+ begin_checkout: "InitiateCheckout",
136
+ add_payment_info: "AddPaymentInfo",
137
+ purchase: "CompletePayment",
138
+ search: "Search",
139
+ sign_up: "CompleteRegistration"
140
+ };
123
141
  var RTL_LOCALES = /* @__PURE__ */ new Set(["ar", "he", "fa", "ur", "yi"]);
124
142
  function getDirectionForLocale(locale) {
125
143
  if (!locale) return "ltr";
@@ -174,6 +192,12 @@ var _BrainerceClient = class _BrainerceClient {
174
192
  // GA4 stitch state (see `loadGoogleAnalytics()` in the Analytics section).
175
193
  this._ga4MeasurementId = null;
176
194
  this._ga4StitchPromise = null;
195
+ // Marketing tags booted by `initTracking()`. Each holds the id already
196
+ // loaded, which is what makes a repeat call a no-op instead of a second
197
+ // pixel install (and a doubled PageView).
198
+ this._gtmContainerId = null;
199
+ this._metaPixelId = null;
200
+ this._tiktokPixelId = null;
177
201
  /** One warning per client, not per keystroke-driven address submit. */
178
202
  this._warnedResolvedOnlyAddressFields = false;
179
203
  /** localStorage key for session cart reference (sessionToken + cartId) */
@@ -1195,6 +1219,225 @@ var _BrainerceClient = class _BrainerceClient {
1195
1219
  }
1196
1220
  });
1197
1221
  }
1222
+ /**
1223
+ * Boot every marketing tag the merchant has configured — GA4, Google Tag
1224
+ * Manager, the Meta pixel, the TikTok pixel — in one call.
1225
+ *
1226
+ * Pass `storeInfo.tracking` straight through. The ids in it are resolved
1227
+ * server-side from the marketplace apps the merchant already connected, so
1228
+ * for the common case nobody types an id anywhere and nobody redeploys the
1229
+ * storefront: connect the Google app in the dashboard and this call starts
1230
+ * loading GA4 on the next page render.
1231
+ *
1232
+ * Call it once, as early as possible (root layout / app entry). It is
1233
+ * idempotent, a no-op during SSR, and never throws — a blocked or missing
1234
+ * tag must never take a storefront down with it.
1235
+ *
1236
+ * GA4 goes through {@link loadGoogleAnalytics}, so the `client_id` /
1237
+ * `session_id` stitch ids keep flowing onto cart and checkout calls and the
1238
+ * server-side purchase conversion still lands in the right session.
1239
+ *
1240
+ * @example
1241
+ * ```typescript
1242
+ * const storeInfo = await client.getStoreInfo();
1243
+ * client.initTracking(storeInfo.tracking);
1244
+ * // …later, on the order confirmation page:
1245
+ * client.trackMarketingEvent('purchase', {
1246
+ * transactionId: order.id,
1247
+ * currency: order.currency,
1248
+ * value: order.totalAmount,
1249
+ * items: order.items.map((i) => ({ itemId: i.sku, itemName: i.name, price: i.price, quantity: i.quantity })),
1250
+ * });
1251
+ * ```
1252
+ */
1253
+ initTracking(tracking) {
1254
+ if (typeof window === "undefined" || !tracking) return;
1255
+ if (tracking.ga4MeasurementId) {
1256
+ this.loadGoogleAnalytics(tracking.ga4MeasurementId);
1257
+ }
1258
+ if (tracking.gtmContainerId) this.loadGtm(tracking.gtmContainerId);
1259
+ if (tracking.metaPixelId) this.loadMetaPixel(tracking.metaPixelId);
1260
+ if (tracking.tiktokPixelId) this.loadTikTokPixel(tracking.tiktokPixelId);
1261
+ }
1262
+ /**
1263
+ * Report one e-commerce event to every marketing tag that
1264
+ * {@link initTracking} loaded (GA4/GTM, Meta, TikTok).
1265
+ *
1266
+ * Distinct from {@link trackEvent}, which posts a cookieless pageview/beacon
1267
+ * to Brainerce's own storefront analytics. This one is about ad platforms —
1268
+ * call both; they answer different questions.
1269
+ *
1270
+ * You describe what happened once, in GA4's vocabulary, and the SDK
1271
+ * translates: a `dataLayer` push for GA4/GTM, the matching Meta standard
1272
+ * event via `fbq`, and the matching TikTok event via `ttq`. Tags that aren't
1273
+ * loaded are skipped silently, so the same call is correct whether the
1274
+ * merchant has connected none, one, or all of them.
1275
+ *
1276
+ * Why this matters for ad spend: a GTM container with no `dataLayer` events
1277
+ * is an empty container, and Meta cannot optimize a campaign it never sees a
1278
+ * `Purchase` for. The value/currency/item-id triple in {@link TrackingEventPayload}
1279
+ * is the whole input to that optimization.
1280
+ *
1281
+ * `purchase` is de-duplicated by the vendors on `transactionId` (GA4
1282
+ * `transaction_id`, Meta `eventID`, TikTok `event_id`), so a shopper
1283
+ * refreshing the confirmation page cannot double-count the order — pass the
1284
+ * order id and the safety is automatic.
1285
+ *
1286
+ * SSR-safe and never throws.
1287
+ */
1288
+ trackMarketingEvent(name, payload = {}) {
1289
+ if (typeof window === "undefined") return;
1290
+ try {
1291
+ const items = payload.items ?? [];
1292
+ window.dataLayer = window.dataLayer || [];
1293
+ window.dataLayer.push({ ecommerce: null });
1294
+ window.dataLayer.push({
1295
+ event: name,
1296
+ ecommerce: {
1297
+ ...payload.currency ? { currency: payload.currency } : {},
1298
+ ...payload.value !== void 0 ? { value: payload.value } : {},
1299
+ ...payload.transactionId ? { transaction_id: payload.transactionId } : {},
1300
+ ...payload.shipping !== void 0 ? { shipping: payload.shipping } : {},
1301
+ ...payload.tax !== void 0 ? { tax: payload.tax } : {},
1302
+ ...payload.coupon ? { coupon: payload.coupon } : {},
1303
+ items: items.map((item) => ({
1304
+ item_id: item.itemId,
1305
+ ...item.itemName ? { item_name: item.itemName } : {},
1306
+ ...item.price !== void 0 ? { price: item.price } : {},
1307
+ ...item.quantity !== void 0 ? { quantity: item.quantity } : {},
1308
+ ...item.itemVariant ? { item_variant: item.itemVariant } : {},
1309
+ ...item.itemCategory ? { item_category: item.itemCategory } : {}
1310
+ }))
1311
+ }
1312
+ });
1313
+ if (window.gtag && this._ga4MeasurementId) {
1314
+ window.gtag("event", name, {
1315
+ ...payload.currency ? { currency: payload.currency } : {},
1316
+ ...payload.value !== void 0 ? { value: payload.value } : {},
1317
+ ...payload.transactionId ? { transaction_id: payload.transactionId } : {},
1318
+ ...payload.shipping !== void 0 ? { shipping: payload.shipping } : {},
1319
+ ...payload.tax !== void 0 ? { tax: payload.tax } : {},
1320
+ ...payload.coupon ? { coupon: payload.coupon } : {},
1321
+ items: items.map((item) => ({
1322
+ item_id: item.itemId,
1323
+ item_name: item.itemName,
1324
+ price: item.price,
1325
+ quantity: item.quantity
1326
+ }))
1327
+ });
1328
+ }
1329
+ const metaEvent = META_EVENT_NAMES[name];
1330
+ if (window.fbq && metaEvent) {
1331
+ const contents = items.map((item) => ({
1332
+ id: item.itemId,
1333
+ quantity: item.quantity ?? 1,
1334
+ ...item.price !== void 0 ? { item_price: item.price } : {}
1335
+ }));
1336
+ window.fbq(
1337
+ "track",
1338
+ metaEvent,
1339
+ {
1340
+ ...payload.currency ? { currency: payload.currency } : {},
1341
+ ...payload.value !== void 0 ? { value: payload.value } : {},
1342
+ ...contents.length ? { contents, content_ids: contents.map((c) => c.id), content_type: "product" } : {}
1343
+ },
1344
+ // Meta de-dupes a browser event against a server (CAPI) event of the
1345
+ // same eventID, and against a repeat send of the same page.
1346
+ payload.transactionId ? { eventID: payload.transactionId } : void 0
1347
+ );
1348
+ }
1349
+ const tiktokEvent = TIKTOK_EVENT_NAMES[name];
1350
+ if (window.ttq?.track && tiktokEvent) {
1351
+ window.ttq.track(
1352
+ tiktokEvent,
1353
+ {
1354
+ ...payload.currency ? { currency: payload.currency } : {},
1355
+ ...payload.value !== void 0 ? { value: payload.value } : {},
1356
+ contents: items.map((item) => ({
1357
+ content_id: item.itemId,
1358
+ content_name: item.itemName,
1359
+ quantity: item.quantity ?? 1,
1360
+ price: item.price
1361
+ }))
1362
+ },
1363
+ payload.transactionId ? { event_id: payload.transactionId } : void 0
1364
+ );
1365
+ }
1366
+ } catch {
1367
+ }
1368
+ }
1369
+ /** Install the GTM container loader. Idempotent; no-op if already present. */
1370
+ loadGtm(containerId) {
1371
+ if (this._gtmContainerId === containerId) return;
1372
+ this._gtmContainerId = containerId;
1373
+ try {
1374
+ window.dataLayer = window.dataLayer || [];
1375
+ window.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
1376
+ const script = document.createElement("script");
1377
+ script.async = true;
1378
+ script.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(containerId)}`;
1379
+ document.head.appendChild(script);
1380
+ } catch {
1381
+ }
1382
+ }
1383
+ /** Install the Meta pixel and fire its initial PageView. Idempotent. */
1384
+ loadMetaPixel(pixelId) {
1385
+ if (this._metaPixelId === pixelId) return;
1386
+ this._metaPixelId = pixelId;
1387
+ try {
1388
+ if (!window.fbq) {
1389
+ const queue = [];
1390
+ const fbq = ((...args) => {
1391
+ if (fbq.callMethod) fbq.callMethod(...args);
1392
+ else queue.push(args);
1393
+ });
1394
+ fbq.queue = queue;
1395
+ fbq.loaded = true;
1396
+ fbq.version = "2.0";
1397
+ window.fbq = fbq;
1398
+ window._fbq = fbq;
1399
+ const script = document.createElement("script");
1400
+ script.async = true;
1401
+ script.src = "https://connect.facebook.net/en_US/fbevents.js";
1402
+ document.head.appendChild(script);
1403
+ }
1404
+ window.fbq("init", pixelId);
1405
+ window.fbq("track", "PageView");
1406
+ } catch {
1407
+ }
1408
+ }
1409
+ /** Install the TikTok pixel and fire its initial page view. Idempotent. */
1410
+ loadTikTokPixel(pixelId) {
1411
+ if (this._tiktokPixelId === pixelId) return;
1412
+ this._tiktokPixelId = pixelId;
1413
+ try {
1414
+ const ttq = window.ttq ?? {};
1415
+ ttq._i = ttq._i ?? {};
1416
+ ttq._i[pixelId] = ttq._i[pixelId] ?? [];
1417
+ ttq._t = ttq._t ?? {};
1418
+ ttq._t[pixelId] = Date.now();
1419
+ ttq._o = ttq._o ?? {};
1420
+ ttq._o[pixelId] = {};
1421
+ const methods = ["page", "track", "identify", "instances", "ready"];
1422
+ ttq.methods = methods;
1423
+ for (const method of methods) {
1424
+ if (typeof ttq[method] !== "function") {
1425
+ ttq[method] = (...args) => {
1426
+ ttq._i?.[pixelId]?.push([method, ...args]);
1427
+ };
1428
+ }
1429
+ }
1430
+ window.ttq = ttq;
1431
+ const script = document.createElement("script");
1432
+ script.async = true;
1433
+ script.src = `https://analytics.tiktok.com/i18n/pixel/events.js?sdkid=${encodeURIComponent(
1434
+ pixelId
1435
+ )}&lib=ttq`;
1436
+ document.head.appendChild(script);
1437
+ ttq.page?.();
1438
+ } catch {
1439
+ }
1440
+ }
1198
1441
  /**
1199
1442
  * Merge the resolved GA4 stitch ids onto a request body — only for fields
1200
1443
  * the caller didn't already set explicitly (explicit values always win).
@@ -5115,9 +5358,7 @@ var _BrainerceClient = class _BrainerceClient {
5115
5358
  * ```
5116
5359
  */
5117
5360
  async setShippingAddress(checkoutId, address) {
5118
- const body = await this.withAnalyticsStitchIds(
5119
- this.stripResolvedOnlyAddressFields(address)
5120
- );
5361
+ const body = await this.withAnalyticsStitchIds(this.stripResolvedOnlyAddressFields(address));
5121
5362
  if (this.isVibeCodedMode()) {
5122
5363
  return this.vibeCodedRequest(
5123
5364
  "PATCH",
@@ -8174,6 +8415,42 @@ var _BrainerceClient = class _BrainerceClient {
8174
8415
  `/api/v1/regions/${encodePathSegment(regionId)}/compatible-providers`
8175
8416
  );
8176
8417
  }
8418
+ /**
8419
+ * List a region's manual price overrides (regional pricing, admin). Prices
8420
+ * are in the region's currency; `variantId: null` rows are product-level.
8421
+ */
8422
+ async getRegionPrices(regionId, params = {}) {
8423
+ return this.adminRequest(
8424
+ "GET",
8425
+ `/api/v1/regions/${encodePathSegment(regionId)}/prices`,
8426
+ void 0,
8427
+ {
8428
+ productId: params.productId,
8429
+ page: params.page,
8430
+ limit: params.limit
8431
+ }
8432
+ );
8433
+ }
8434
+ /**
8435
+ * Bulk upsert/remove manual price overrides for a region (admin). Each
8436
+ * entry: `price` = regular, `salePrice` = sale (must be lower), in the
8437
+ * REGION's currency; `remove: true` deletes. A product/variant with no
8438
+ * entry keeps automatic FX conversion.
8439
+ */
8440
+ async upsertRegionPrices(regionId, entries) {
8441
+ return this.adminRequest(
8442
+ "PUT",
8443
+ `/api/v1/regions/${encodePathSegment(regionId)}/prices`,
8444
+ { entries }
8445
+ );
8446
+ }
8447
+ /** Delete one manual price override by id (admin). */
8448
+ async deleteRegionPrice(regionId, priceId) {
8449
+ await this.adminRequest(
8450
+ "DELETE",
8451
+ `/api/v1/regions/${encodePathSegment(regionId)}/prices/${encodePathSegment(priceId)}`
8452
+ );
8453
+ }
8177
8454
  // -------------------- Regions (Storefront + vibe-coded, public — no apiKey) --------------------
8178
8455
  // storeId- or connectionId-based, no auth. Call these from a storefront to
8179
8456
  // detect the buyer's region (story S1), then pair with detectRegion(). Then
@@ -10123,7 +10400,10 @@ function getCartTotals(cart, shippingPrice) {
10123
10400
  return { subtotal, discount, shipping, total };
10124
10401
  }
10125
10402
  function getCartItemName(item) {
10126
- return item.variant?.name || item.product.name;
10403
+ const productName = item.product.name;
10404
+ const variantName = item.variant?.name?.trim();
10405
+ if (!variantName || variantName === productName) return productName;
10406
+ return `${productName} - ${variantName}`;
10127
10407
  }
10128
10408
  function getCartItemImage(item) {
10129
10409
  if (item.variant?.image) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainerce",
3
- "version": "1.53.1",
3
+ "version": "1.55.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",
@@ -28,6 +28,7 @@
28
28
  "lint": "eslint \"src/**/*.ts\"",
29
29
  "test": "vitest run",
30
30
  "test:watch": "vitest",
31
+ "release": "pnpm build && node ../../scripts/publish-workspace-package.js .",
31
32
  "prepublishOnly": "pnpm build"
32
33
  },
33
34
  "keywords": [