brainerce 1.55.0 → 1.57.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 +83 -7
- package/dist/index.d.mts +201 -2
- package/dist/index.d.ts +201 -2
- package/dist/index.js +233 -12
- package/dist/index.mjs +232 -12
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -63,6 +63,7 @@ __export(index_exports, {
|
|
|
63
63
|
getProductMetafieldsByType: () => getProductMetafieldsByType,
|
|
64
64
|
getProductPrice: () => getProductPrice,
|
|
65
65
|
getProductPriceInfo: () => getProductPriceInfo,
|
|
66
|
+
getProductSitemapEntries: () => getProductSitemapEntries,
|
|
66
67
|
getProductSwatches: () => getProductSwatches,
|
|
67
68
|
getStockStatus: () => getStockStatus,
|
|
68
69
|
getVariantOptions: () => getVariantOptions,
|
|
@@ -298,6 +299,14 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
298
299
|
* This is needed because Stripe redirects lose in-memory state.
|
|
299
300
|
*/
|
|
300
301
|
this.ACTIVE_CHECKOUT_KEY = "brainerce_active_checkout";
|
|
302
|
+
/**
|
|
303
|
+
* Merge the resolved GA4 stitch ids onto a request body — only for fields
|
|
304
|
+
* the caller didn't already set explicitly (explicit values always win).
|
|
305
|
+
* No-op (returns `dto` unchanged) if `loadGoogleAnalytics()` was never
|
|
306
|
+
* called, or if it hasn't resolved any ids by the time this is awaited.
|
|
307
|
+
*/
|
|
308
|
+
/** localStorage key holding the last non-direct-touch attribution blob. */
|
|
309
|
+
this.TRAFFIC_ATTR_KEY = "brainerce_attr";
|
|
301
310
|
// -------------------- Contact Forms (schema) --------------------
|
|
302
311
|
/**
|
|
303
312
|
* List active contact forms configured for the store.
|
|
@@ -703,6 +712,7 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
703
712
|
this.onCartReset = options.onCartReset;
|
|
704
713
|
this.hydrateSessionCart();
|
|
705
714
|
this.detectRecoverCartFromUrl();
|
|
715
|
+
this.captureTrafficAttribution();
|
|
706
716
|
}
|
|
707
717
|
// -------------------- Locale --------------------
|
|
708
718
|
/**
|
|
@@ -1525,11 +1535,82 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
1525
1535
|
}
|
|
1526
1536
|
}
|
|
1527
1537
|
/**
|
|
1528
|
-
*
|
|
1529
|
-
*
|
|
1530
|
-
*
|
|
1531
|
-
*
|
|
1538
|
+
* Record the visit's traffic origin — LAST NON-DIRECT TOUCH semantics: an
|
|
1539
|
+
* external referrer or any utm_source overwrites the stored blob; a direct
|
|
1540
|
+
* or internal navigation keeps the previous touch. Runs once per client
|
|
1541
|
+
* construction (i.e. per page load in browser storefronts) and must never
|
|
1542
|
+
* throw — attribution is telemetry, the storefront always wins.
|
|
1543
|
+
*/
|
|
1544
|
+
captureTrafficAttribution() {
|
|
1545
|
+
try {
|
|
1546
|
+
if (typeof window === "undefined" || !window.localStorage) return;
|
|
1547
|
+
const params = new URLSearchParams(window.location.search);
|
|
1548
|
+
const utmSource = params.get("utm_source") || void 0;
|
|
1549
|
+
const utmMedium = params.get("utm_medium") || void 0;
|
|
1550
|
+
const utmCampaign = params.get("utm_campaign") || void 0;
|
|
1551
|
+
let referrerHost;
|
|
1552
|
+
if (document.referrer) {
|
|
1553
|
+
try {
|
|
1554
|
+
const ref = new URL(document.referrer);
|
|
1555
|
+
if (ref.hostname && ref.hostname !== window.location.hostname) {
|
|
1556
|
+
referrerHost = ref.hostname.toLowerCase().replace(/^www\./, "");
|
|
1557
|
+
}
|
|
1558
|
+
} catch {
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
if (!referrerHost && !utmSource) return;
|
|
1562
|
+
window.localStorage.setItem(
|
|
1563
|
+
this.TRAFFIC_ATTR_KEY,
|
|
1564
|
+
JSON.stringify({ referrerHost, utmSource, utmMedium, utmCampaign, at: Date.now() })
|
|
1565
|
+
);
|
|
1566
|
+
} catch {
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
/** The stored attribution as request-body fields, or null when absent/stale. */
|
|
1570
|
+
getTrafficAttribution() {
|
|
1571
|
+
try {
|
|
1572
|
+
if (typeof window === "undefined" || !window.localStorage) return null;
|
|
1573
|
+
const raw = window.localStorage.getItem(this.TRAFFIC_ATTR_KEY);
|
|
1574
|
+
if (!raw) return null;
|
|
1575
|
+
const blob = JSON.parse(raw);
|
|
1576
|
+
if (!blob || typeof blob !== "object") return null;
|
|
1577
|
+
if (typeof blob.at !== "number" || Date.now() - blob.at > _BrainerceClient.TRAFFIC_ATTR_MAX_AGE_MS) {
|
|
1578
|
+
return null;
|
|
1579
|
+
}
|
|
1580
|
+
const out = {};
|
|
1581
|
+
if (typeof blob.referrerHost === "string" && blob.referrerHost) {
|
|
1582
|
+
out.trafficReferrerHost = blob.referrerHost.slice(0, 253);
|
|
1583
|
+
}
|
|
1584
|
+
if (typeof blob.utmSource === "string" && blob.utmSource) {
|
|
1585
|
+
out.trafficUtmSource = blob.utmSource.slice(0, 150);
|
|
1586
|
+
}
|
|
1587
|
+
if (typeof blob.utmMedium === "string" && blob.utmMedium) {
|
|
1588
|
+
out.trafficUtmMedium = blob.utmMedium.slice(0, 150);
|
|
1589
|
+
}
|
|
1590
|
+
if (typeof blob.utmCampaign === "string" && blob.utmCampaign) {
|
|
1591
|
+
out.trafficUtmCampaign = blob.utmCampaign.slice(0, 150);
|
|
1592
|
+
}
|
|
1593
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
1594
|
+
} catch {
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Merge the stored traffic attribution onto a request body — only for
|
|
1600
|
+
* fields the caller didn't set explicitly (explicit values always win),
|
|
1601
|
+
* mirroring `withAnalyticsStitchIds`. No-op outside the browser.
|
|
1532
1602
|
*/
|
|
1603
|
+
withTrafficAttribution(dto) {
|
|
1604
|
+
const attr = this.getTrafficAttribution();
|
|
1605
|
+
if (!attr) return dto;
|
|
1606
|
+
return {
|
|
1607
|
+
...dto ?? {},
|
|
1608
|
+
trafficReferrerHost: dto?.trafficReferrerHost ?? attr.trafficReferrerHost,
|
|
1609
|
+
trafficUtmSource: dto?.trafficUtmSource ?? attr.trafficUtmSource,
|
|
1610
|
+
trafficUtmMedium: dto?.trafficUtmMedium ?? attr.trafficUtmMedium,
|
|
1611
|
+
trafficUtmCampaign: dto?.trafficUtmCampaign ?? attr.trafficUtmCampaign
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1533
1614
|
async withAnalyticsStitchIds(dto) {
|
|
1534
1615
|
if (!this._ga4StitchPromise) return dto;
|
|
1535
1616
|
try {
|
|
@@ -1653,6 +1734,50 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
1653
1734
|
queryParamsWithRegion
|
|
1654
1735
|
);
|
|
1655
1736
|
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Lightweight product rows for sitemap generation: `slug`, `updatedAt`, and
|
|
1739
|
+
* per-locale `localeSlugs` only — up to `limit` (max 5000) in ONE request,
|
|
1740
|
+
* with none of the 100-per-page clamp the full listing applies.
|
|
1741
|
+
*
|
|
1742
|
+
* Sales-channel (`salesChannelId`) mode only; other modes throw so
|
|
1743
|
+
* {@link getProductSitemapEntries} (sitemap.ts) can catch and fall back to
|
|
1744
|
+
* paginating `getProducts`. Prefer that helper over calling this directly.
|
|
1745
|
+
*/
|
|
1746
|
+
async getSitemapProducts(limit = 5e3) {
|
|
1747
|
+
if (!this.isVibeCodedMode()) {
|
|
1748
|
+
throw new Error("getSitemapProducts is available in salesChannelId mode only");
|
|
1749
|
+
}
|
|
1750
|
+
const res = await this.vibeCodedRequest("GET", "/sitemap-products", void 0, { limit: Math.min(limit, 5e3) });
|
|
1751
|
+
return res.data;
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Resolve an old (renamed) slug to the entity's CURRENT slug, so the
|
|
1755
|
+
* storefront can issue a permanent (301/308) redirect instead of a 404.
|
|
1756
|
+
*
|
|
1757
|
+
* Call this in the catch/not-found path of a product or blog page:
|
|
1758
|
+
* the platform records every slug rename, so a URL that stopped matching
|
|
1759
|
+
* usually has a redirect. Returns `null` when there is no redirect (real
|
|
1760
|
+
* 404) or outside sales-channel mode — always fall through to notFound().
|
|
1761
|
+
*
|
|
1762
|
+
* @example
|
|
1763
|
+
* ```typescript
|
|
1764
|
+
* // app/products/[slug]/page.tsx — in the catch path:
|
|
1765
|
+
* const redirect = await client.resolveSlugRedirect('product', slug);
|
|
1766
|
+
* if (redirect) permanentRedirect(`/products/${redirect.currentSlug}`);
|
|
1767
|
+
* notFound();
|
|
1768
|
+
* ```
|
|
1769
|
+
*/
|
|
1770
|
+
async resolveSlugRedirect(entityType, slug) {
|
|
1771
|
+
if (!this.isVibeCodedMode()) return null;
|
|
1772
|
+
try {
|
|
1773
|
+
return await this.vibeCodedRequest(
|
|
1774
|
+
"GET",
|
|
1775
|
+
`/slug-redirects/${entityType}/${encodePathSegment(slug)}`
|
|
1776
|
+
);
|
|
1777
|
+
} catch {
|
|
1778
|
+
return null;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1656
1781
|
/**
|
|
1657
1782
|
* Get a single product by ID
|
|
1658
1783
|
* Works in vibe-coded, storefront (public), and admin mode
|
|
@@ -5342,7 +5467,7 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
5342
5467
|
* ```
|
|
5343
5468
|
*/
|
|
5344
5469
|
async setCheckoutCustomer(checkoutId, data) {
|
|
5345
|
-
const body = await this.withAnalyticsStitchIds(data);
|
|
5470
|
+
const body = this.withTrafficAttribution(await this.withAnalyticsStitchIds(data));
|
|
5346
5471
|
if (this.isVibeCodedMode()) {
|
|
5347
5472
|
return this.vibeCodedRequest(
|
|
5348
5473
|
"PATCH",
|
|
@@ -5444,7 +5569,9 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
5444
5569
|
* ```
|
|
5445
5570
|
*/
|
|
5446
5571
|
async setShippingAddress(checkoutId, address) {
|
|
5447
|
-
const body =
|
|
5572
|
+
const body = this.withTrafficAttribution(
|
|
5573
|
+
await this.withAnalyticsStitchIds(this.stripResolvedOnlyAddressFields(address))
|
|
5574
|
+
);
|
|
5448
5575
|
if (this.isVibeCodedMode()) {
|
|
5449
5576
|
return this.vibeCodedRequest(
|
|
5450
5577
|
"PATCH",
|
|
@@ -8945,6 +9072,41 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
8945
9072
|
{ salesChannelId }
|
|
8946
9073
|
);
|
|
8947
9074
|
}
|
|
9075
|
+
/**
|
|
9076
|
+
* Attach a customer to a sales channel (admin mode) — marks them as active in
|
|
9077
|
+
* that storefront. Accepts the sales-channel record ID or its public `vc_*`
|
|
9078
|
+
* connection ID.
|
|
9079
|
+
*
|
|
9080
|
+
* Rarely needed: the platform records a channel by itself whenever the
|
|
9081
|
+
* customer registers, signs in or checks out on it. Use this for migrations
|
|
9082
|
+
* from another system and for fixing up records you created yourself. A
|
|
9083
|
+
* customer belongs to one store but can be active in any number of its
|
|
9084
|
+
* channels, so calling this for several channels is normal and expected.
|
|
9085
|
+
*
|
|
9086
|
+
* This does not change where the customer CAME FROM — for that, pass
|
|
9087
|
+
* `acquisitionSalesChannelId` to {@link updateCustomer}.
|
|
9088
|
+
*/
|
|
9089
|
+
async publishCustomerToSalesChannel(customerId, salesChannelId) {
|
|
9090
|
+
return this.adminRequest(
|
|
9091
|
+
"POST",
|
|
9092
|
+
`/api/v1/customers/${encodePathSegment(customerId)}/publish-sales-channel`,
|
|
9093
|
+
{ salesChannelId }
|
|
9094
|
+
);
|
|
9095
|
+
}
|
|
9096
|
+
/**
|
|
9097
|
+
* Detach a customer from a sales channel (admin mode).
|
|
9098
|
+
*
|
|
9099
|
+
* A correction, NOT a block — it does not stop that person from buying on
|
|
9100
|
+
* that storefront, and the channel is recorded again the next time they sign
|
|
9101
|
+
* in or order there. There is no way to bar a customer from a channel.
|
|
9102
|
+
*/
|
|
9103
|
+
async unpublishCustomerFromSalesChannel(customerId, salesChannelId) {
|
|
9104
|
+
return this.adminRequest(
|
|
9105
|
+
"POST",
|
|
9106
|
+
`/api/v1/customers/${encodePathSegment(customerId)}/unpublish-sales-channel`,
|
|
9107
|
+
{ salesChannelId }
|
|
9108
|
+
);
|
|
9109
|
+
}
|
|
8948
9110
|
/**
|
|
8949
9111
|
* Publish a coupon to a sales channel (admin mode) — makes it redeemable on
|
|
8950
9112
|
* that vibe-coded storefront. Accepts the sales-channel record ID or its
|
|
@@ -9636,6 +9798,8 @@ _BrainerceClient.RESOLVED_ONLY_ADDRESS_FIELDS = [
|
|
|
9636
9798
|
"lng",
|
|
9637
9799
|
"formattedAddress"
|
|
9638
9800
|
];
|
|
9801
|
+
/** Attribution older than this is stale and never forwarded (classic 30-day window). */
|
|
9802
|
+
_BrainerceClient.TRAFFIC_ATTR_MAX_AGE_MS = 30 * 24 * 3600 * 1e3;
|
|
9639
9803
|
var BrainerceClient = _BrainerceClient;
|
|
9640
9804
|
var BrainerceError = class extends Error {
|
|
9641
9805
|
constructor(message, statusCode, details) {
|
|
@@ -10136,7 +10300,8 @@ function buildProductJsonLd(product, opts) {
|
|
|
10136
10300
|
const brand = opts.brandName ?? product.brands?.[0]?.name;
|
|
10137
10301
|
const description = stripHtml(product.description).slice(0, 5e3);
|
|
10138
10302
|
const effectivePrice = product.salePrice ?? product.basePrice;
|
|
10139
|
-
const
|
|
10303
|
+
const inv = product.inventory;
|
|
10304
|
+
const availability = !inv ? "https://schema.org/InStock" : inv.inStock ?? (inv.available ?? 0) > 0 ? "https://schema.org/InStock" : inv.canPurchase ? "https://schema.org/BackOrder" : "https://schema.org/OutOfStock";
|
|
10140
10305
|
const isVariable = product.type === "VARIABLE" && product.priceMin && product.priceMax;
|
|
10141
10306
|
const itemCondition = "https://schema.org/NewCondition";
|
|
10142
10307
|
const shippingDetails = (opts.shipping ?? []).filter((z) => z.amount !== null).map((z) => ({
|
|
@@ -10167,8 +10332,9 @@ function buildProductJsonLd(product, opts) {
|
|
|
10167
10332
|
"@type": "AggregateOffer",
|
|
10168
10333
|
lowPrice: product.priceMin,
|
|
10169
10334
|
highPrice: product.priceMax,
|
|
10335
|
+
...product.variants?.length ? { offerCount: product.variants.length } : {},
|
|
10170
10336
|
priceCurrency: opts.currency,
|
|
10171
|
-
availability
|
|
10337
|
+
availability,
|
|
10172
10338
|
itemCondition,
|
|
10173
10339
|
...shippingDetails.length > 0 ? { shippingDetails } : {},
|
|
10174
10340
|
...url ? { url } : {}
|
|
@@ -10176,7 +10342,7 @@ function buildProductJsonLd(product, opts) {
|
|
|
10176
10342
|
"@type": "Offer",
|
|
10177
10343
|
price: effectivePrice,
|
|
10178
10344
|
priceCurrency: opts.currency,
|
|
10179
|
-
availability
|
|
10345
|
+
availability,
|
|
10180
10346
|
itemCondition,
|
|
10181
10347
|
// Only meaningful for an active sale price with a known end date —
|
|
10182
10348
|
// a regular (non-sale) price has no expiry to declare.
|
|
@@ -10191,19 +10357,25 @@ function buildProductJsonLd(product, opts) {
|
|
|
10191
10357
|
...description ? { description } : {},
|
|
10192
10358
|
...images.length > 0 ? { image: images } : {},
|
|
10193
10359
|
...url ? { url } : {},
|
|
10194
|
-
|
|
10360
|
+
// Fall back to the product id so the offer always carries a stable SKU —
|
|
10361
|
+
// Merchant Center matching and review aggregation both key on it.
|
|
10362
|
+
sku: product.sku || product.id,
|
|
10195
10363
|
// Identifiers Google uses to match a product to its catalog — key for
|
|
10196
10364
|
// free merchant-listing eligibility without a Merchant Center feed.
|
|
10197
10365
|
...product.gtin ? { gtin: product.gtin } : {},
|
|
10198
10366
|
...product.mpn ? { mpn: product.mpn } : {},
|
|
10199
10367
|
...brand ? { brand: { "@type": "Brand", name: brand } } : {},
|
|
10200
10368
|
offers: offer,
|
|
10201
|
-
// Google policy: never emit an empty/zero rating block.
|
|
10369
|
+
// Google policy: never emit an empty/zero rating block. bestRating /
|
|
10370
|
+
// worstRating make the 1-5 scale explicit so aggregators can't misread
|
|
10371
|
+
// a 4.8 on an assumed 0-10 scale.
|
|
10202
10372
|
...product.reviewCount && product.reviewCount > 0 && product.avgRating ? {
|
|
10203
10373
|
aggregateRating: {
|
|
10204
10374
|
"@type": "AggregateRating",
|
|
10205
10375
|
ratingValue: product.avgRating,
|
|
10206
|
-
reviewCount: product.reviewCount
|
|
10376
|
+
reviewCount: product.reviewCount,
|
|
10377
|
+
bestRating: 5,
|
|
10378
|
+
worstRating: 1
|
|
10207
10379
|
}
|
|
10208
10380
|
} : {}
|
|
10209
10381
|
};
|
|
@@ -10314,6 +10486,54 @@ async function getBlogSitemapEntries(client, opts) {
|
|
|
10314
10486
|
}
|
|
10315
10487
|
return entries;
|
|
10316
10488
|
}
|
|
10489
|
+
async function getProductSitemapEntries(client, opts) {
|
|
10490
|
+
const base = opts.siteUrl.replace(/\/+$/, "");
|
|
10491
|
+
const basePath = opts.basePath ?? "/products";
|
|
10492
|
+
const pageSize = Math.min(opts.pageSize ?? 100, 100);
|
|
10493
|
+
const maxEntries = opts.maxEntries ?? 5e3;
|
|
10494
|
+
let rows = [];
|
|
10495
|
+
try {
|
|
10496
|
+
rows = await client.getSitemapProducts(maxEntries);
|
|
10497
|
+
} catch {
|
|
10498
|
+
let page = 1;
|
|
10499
|
+
for (; ; ) {
|
|
10500
|
+
const res = await client.getProducts({ page, limit: pageSize });
|
|
10501
|
+
rows.push(
|
|
10502
|
+
...res.data.map((p) => ({
|
|
10503
|
+
slug: p.slug ?? null,
|
|
10504
|
+
id: p.id,
|
|
10505
|
+
updatedAt: p.updatedAt,
|
|
10506
|
+
localeSlugs: p.localeSlugs ?? null
|
|
10507
|
+
}))
|
|
10508
|
+
);
|
|
10509
|
+
if (page >= res.meta.totalPages || rows.length >= maxEntries) break;
|
|
10510
|
+
page += 1;
|
|
10511
|
+
}
|
|
10512
|
+
}
|
|
10513
|
+
const nonDefaultLocales = opts.locales?.filter((locale) => locale !== opts.defaultLocale) ?? [];
|
|
10514
|
+
const entries = [];
|
|
10515
|
+
for (const row of rows.slice(0, maxEntries)) {
|
|
10516
|
+
const baseSlug = row.slug || row.id;
|
|
10517
|
+
if (!baseSlug) continue;
|
|
10518
|
+
const lastModified = row.updatedAt ? new Date(row.updatedAt) : void 0;
|
|
10519
|
+
const localeSlugs = row.localeSlugs ?? {};
|
|
10520
|
+
entries.push({
|
|
10521
|
+
url: `${base}${basePath}/${baseSlug}`,
|
|
10522
|
+
...lastModified ? { lastModified } : {},
|
|
10523
|
+
changeFrequency: "daily",
|
|
10524
|
+
priority: 0.8
|
|
10525
|
+
});
|
|
10526
|
+
for (const locale of nonDefaultLocales) {
|
|
10527
|
+
entries.push({
|
|
10528
|
+
url: `${base}/${locale}${basePath}/${localeSlugs[locale] || baseSlug}`,
|
|
10529
|
+
...lastModified ? { lastModified } : {},
|
|
10530
|
+
changeFrequency: "daily",
|
|
10531
|
+
priority: 0.7
|
|
10532
|
+
});
|
|
10533
|
+
}
|
|
10534
|
+
}
|
|
10535
|
+
return entries;
|
|
10536
|
+
}
|
|
10317
10537
|
async function getCategorySitemapEntries(client, opts) {
|
|
10318
10538
|
const base = opts.siteUrl.replace(/\/+$/, "");
|
|
10319
10539
|
const basePath = opts.basePath ?? "/category";
|
|
@@ -10619,6 +10839,7 @@ function isCouponApplicableToProduct(coupon, productId) {
|
|
|
10619
10839
|
getProductMetafieldsByType,
|
|
10620
10840
|
getProductPrice,
|
|
10621
10841
|
getProductPriceInfo,
|
|
10842
|
+
getProductSitemapEntries,
|
|
10622
10843
|
getProductSwatches,
|
|
10623
10844
|
getStockStatus,
|
|
10624
10845
|
getVariantOptions,
|