brainerce 1.50.0 → 1.52.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 +6156 -5888
- package/dist/bot/bootstrap.global.js +10 -10
- package/dist/bot/index.js +5 -2
- package/dist/bot/index.mjs +5 -2
- package/dist/index.d.mts +460 -14
- package/dist/index.d.ts +460 -14
- package/dist/index.js +398 -25
- package/dist/index.mjs +396 -25
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -49,6 +49,7 @@ __export(index_exports, {
|
|
|
49
49
|
formatProductPrice: () => formatProductPrice,
|
|
50
50
|
formatVariantPrice: () => formatVariantPrice,
|
|
51
51
|
getBlogSitemapEntries: () => getBlogSitemapEntries,
|
|
52
|
+
getBusinessHoursForDate: () => getBusinessHoursForDate,
|
|
52
53
|
getCartItemImage: () => getCartItemImage,
|
|
53
54
|
getCartItemName: () => getCartItemName,
|
|
54
55
|
getCartTotals: () => getCartTotals,
|
|
@@ -73,6 +74,7 @@ __export(index_exports, {
|
|
|
73
74
|
isHtmlDescription: () => isHtmlDescription,
|
|
74
75
|
isWebhookEventType: () => isWebhookEventType,
|
|
75
76
|
jsonLdScriptProps: () => jsonLdScriptProps,
|
|
77
|
+
parseDateFieldValue: () => parseDateFieldValue,
|
|
76
78
|
parseWebhookEvent: () => parseWebhookEvent,
|
|
77
79
|
resolveStoreLocalParts: () => resolveStoreLocalParts,
|
|
78
80
|
safePaymentRedirect: () => safePaymentRedirect,
|
|
@@ -199,7 +201,7 @@ function isDevGuardsEnabled() {
|
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
// src/version.ts
|
|
202
|
-
var SDK_VERSION = "1.
|
|
204
|
+
var SDK_VERSION = "1.52.0";
|
|
203
205
|
|
|
204
206
|
// src/client.ts
|
|
205
207
|
var DEFAULT_BASE_URL = "https://api.brainerce.com";
|
|
@@ -255,6 +257,9 @@ var BrainerceClient = class {
|
|
|
255
257
|
* so the recovered cart shows up with zero per-store code.
|
|
256
258
|
*/
|
|
257
259
|
this._pendingRecoverCartId = null;
|
|
260
|
+
// GA4 stitch state (see `loadGoogleAnalytics()` in the Analytics section).
|
|
261
|
+
this._ga4MeasurementId = null;
|
|
262
|
+
this._ga4StitchPromise = null;
|
|
258
263
|
/** localStorage key for session cart reference (sessionToken + cartId) */
|
|
259
264
|
this.SESSION_CART_KEY = "brainerce_session";
|
|
260
265
|
/**
|
|
@@ -1165,6 +1170,126 @@ var BrainerceClient = class {
|
|
|
1165
1170
|
} catch {
|
|
1166
1171
|
}
|
|
1167
1172
|
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Load GA4's `gtag.js` and start resolving the `client_id`/`session_id`
|
|
1175
|
+
* "stitch" ids Google needs to join a later server-side purchase conversion
|
|
1176
|
+
* to this browser's GA4 session (without them, a server-sent purchase event
|
|
1177
|
+
* either gets rejected or creates a phantom user in GA4). Call this once,
|
|
1178
|
+
* as early as possible (app entry / root layout).
|
|
1179
|
+
*
|
|
1180
|
+
* `createCart()`, `addToCart()`, `setCheckoutCustomer()`, and
|
|
1181
|
+
* `setShippingAddress()` all auto-attach the resolved ids once available —
|
|
1182
|
+
* an explicit `analyticsClientId`/`analyticsSessionId` you pass to any of
|
|
1183
|
+
* those always wins over the auto-captured value. You only need to call
|
|
1184
|
+
* this once; every subsequent cart/checkout call benefits automatically.
|
|
1185
|
+
*
|
|
1186
|
+
* Ids are resolved via `gtag('get', measurementId, 'client_id' | 'session_id', cb)`
|
|
1187
|
+
* — Google's documented method — never by parsing the `_ga` cookie, which
|
|
1188
|
+
* is undocumented and breaks silently across cookie-format changes and
|
|
1189
|
+
* Consent Mode v2 states. If the shopper has denied analytics consent,
|
|
1190
|
+
* gtag reports no client_id and the ids are simply omitted — never
|
|
1191
|
+
* synthesized.
|
|
1192
|
+
*
|
|
1193
|
+
* No-op outside the browser (SSR-safe) and never throws — a blocked or
|
|
1194
|
+
* slow gtag just means server-side conversions won't stitch; it never
|
|
1195
|
+
* breaks the storefront or delays checkout by more than `options.timeoutMs`.
|
|
1196
|
+
*
|
|
1197
|
+
* @example
|
|
1198
|
+
* ```typescript
|
|
1199
|
+
* // Call once, e.g. in your root layout / app entry point
|
|
1200
|
+
* client.loadGoogleAnalytics('G-XXXXXXX');
|
|
1201
|
+
*
|
|
1202
|
+
* // Every cart/checkout call from here on auto-forwards the stitch ids —
|
|
1203
|
+
* // no other code changes needed.
|
|
1204
|
+
* const cart = await client.createCart();
|
|
1205
|
+
* await client.addToCart(cart.id, { productId: 'prod_abc', quantity: 1 });
|
|
1206
|
+
* ```
|
|
1207
|
+
*/
|
|
1208
|
+
loadGoogleAnalytics(measurementId, options) {
|
|
1209
|
+
if (typeof window === "undefined" || !measurementId) return;
|
|
1210
|
+
if (this._ga4MeasurementId === measurementId && this._ga4StitchPromise) return;
|
|
1211
|
+
this._ga4MeasurementId = measurementId;
|
|
1212
|
+
try {
|
|
1213
|
+
if (!window.gtag) {
|
|
1214
|
+
window.dataLayer = window.dataLayer || [];
|
|
1215
|
+
const gtag = (...args) => {
|
|
1216
|
+
window.dataLayer.push(args);
|
|
1217
|
+
};
|
|
1218
|
+
window.gtag = gtag;
|
|
1219
|
+
gtag("js", /* @__PURE__ */ new Date());
|
|
1220
|
+
const script = document.createElement("script");
|
|
1221
|
+
script.async = true;
|
|
1222
|
+
script.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`;
|
|
1223
|
+
document.head.appendChild(script);
|
|
1224
|
+
}
|
|
1225
|
+
window.gtag("config", measurementId);
|
|
1226
|
+
} catch {
|
|
1227
|
+
}
|
|
1228
|
+
this._ga4StitchPromise = this.resolveGa4StitchIds(measurementId, options?.timeoutMs ?? 1500);
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Resolve GA4's `client_id`/`session_id` via `gtag('get', ...)`, bounded by
|
|
1232
|
+
* `timeoutMs` so a slow/blocked gtag never hangs a cart/checkout call.
|
|
1233
|
+
* Resolves to `{}` (never rejects) on timeout, missing gtag, or denied
|
|
1234
|
+
* consent.
|
|
1235
|
+
*/
|
|
1236
|
+
resolveGa4StitchIds(measurementId, timeoutMs) {
|
|
1237
|
+
return new Promise((resolve) => {
|
|
1238
|
+
if (typeof window === "undefined" || !window.gtag) {
|
|
1239
|
+
resolve({});
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
const result = {};
|
|
1243
|
+
let settled = false;
|
|
1244
|
+
const finish = () => {
|
|
1245
|
+
if (settled) return;
|
|
1246
|
+
settled = true;
|
|
1247
|
+
resolve(result);
|
|
1248
|
+
};
|
|
1249
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
1250
|
+
let pending = 2;
|
|
1251
|
+
const done = () => {
|
|
1252
|
+
pending -= 1;
|
|
1253
|
+
if (pending === 0) {
|
|
1254
|
+
clearTimeout(timer);
|
|
1255
|
+
finish();
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
try {
|
|
1259
|
+
window.gtag("get", measurementId, "client_id", (id) => {
|
|
1260
|
+
if (id) result.analyticsClientId = id;
|
|
1261
|
+
done();
|
|
1262
|
+
});
|
|
1263
|
+
window.gtag("get", measurementId, "session_id", (id) => {
|
|
1264
|
+
if (id) result.analyticsSessionId = String(id);
|
|
1265
|
+
done();
|
|
1266
|
+
});
|
|
1267
|
+
} catch {
|
|
1268
|
+
clearTimeout(timer);
|
|
1269
|
+
finish();
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Merge the resolved GA4 stitch ids onto a request body — only for fields
|
|
1275
|
+
* the caller didn't already set explicitly (explicit values always win).
|
|
1276
|
+
* No-op (returns `dto` unchanged) if `loadGoogleAnalytics()` was never
|
|
1277
|
+
* called, or if it hasn't resolved any ids by the time this is awaited.
|
|
1278
|
+
*/
|
|
1279
|
+
async withAnalyticsStitchIds(dto) {
|
|
1280
|
+
if (!this._ga4StitchPromise) return dto;
|
|
1281
|
+
try {
|
|
1282
|
+
const ids = await this._ga4StitchPromise;
|
|
1283
|
+
if (!ids.analyticsClientId && !ids.analyticsSessionId) return dto;
|
|
1284
|
+
return {
|
|
1285
|
+
...dto ?? {},
|
|
1286
|
+
analyticsClientId: dto?.analyticsClientId ?? ids.analyticsClientId,
|
|
1287
|
+
analyticsSessionId: dto?.analyticsSessionId ?? ids.analyticsSessionId
|
|
1288
|
+
};
|
|
1289
|
+
} catch {
|
|
1290
|
+
return dto;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1168
1293
|
// -------------------- Products --------------------
|
|
1169
1294
|
/**
|
|
1170
1295
|
* Get a list of products with pagination and filtering
|
|
@@ -1885,13 +2010,23 @@ var BrainerceClient = class {
|
|
|
1885
2010
|
}
|
|
1886
2011
|
/**
|
|
1887
2012
|
* Create a shipping label for an order via the installed App Store shipping app.
|
|
1888
|
-
* Pass the rate ID returned from
|
|
1889
|
-
* to the merchant's carrier account
|
|
2013
|
+
* Pass the rate ID returned from rate shopping — treat it as opaque and never
|
|
2014
|
+
* parse it. Billing goes directly to the merchant's carrier account; Brainerce
|
|
2015
|
+
* is not a billing intermediary.
|
|
2016
|
+
*
|
|
2017
|
+
* `labelFormat` defaults to `PDF`. Use `ZPL` or `EPL` for warehouse thermal
|
|
2018
|
+
* printers. If the carrier cannot produce the requested format it returns its
|
|
2019
|
+
* closest match rather than failing the purchase — check the response.
|
|
2020
|
+
*
|
|
2021
|
+
* Once the label exists, tracking updates arrive automatically: the carrier's
|
|
2022
|
+
* webhooks move the shipment through in-transit → delivered and complete the
|
|
2023
|
+
* order. No polling required.
|
|
1890
2024
|
*
|
|
1891
2025
|
* @example
|
|
1892
2026
|
* ```typescript
|
|
1893
2027
|
* const label = await client.createShippingLabel('order_abc', {
|
|
1894
2028
|
* rateId: 'rate_8f123456789abcdef',
|
|
2029
|
+
* labelFormat: 'ZPL',
|
|
1895
2030
|
* });
|
|
1896
2031
|
* console.log('Label URL:', label.labelUrl);
|
|
1897
2032
|
* console.log('Tracking:', label.trackingNumber);
|
|
@@ -1904,6 +2039,37 @@ var BrainerceClient = class {
|
|
|
1904
2039
|
data
|
|
1905
2040
|
);
|
|
1906
2041
|
}
|
|
2042
|
+
/**
|
|
2043
|
+
* Live carrier rates for an order, from the merchant's installed shipping app.
|
|
2044
|
+
*
|
|
2045
|
+
* Call this immediately before {@link createShippingLabel} and pass the chosen
|
|
2046
|
+
* `id` straight through — the id is opaque and must not be parsed. It is also
|
|
2047
|
+
* short-lived: this call is what creates the shipment at the carrier, so a
|
|
2048
|
+
* rate from an old call may no longer be purchasable.
|
|
2049
|
+
*
|
|
2050
|
+
* Returns `[]` when the store has no shipping app installed, or when the
|
|
2051
|
+
* order has no usable shipping address.
|
|
2052
|
+
*
|
|
2053
|
+
* @example
|
|
2054
|
+
* ```typescript
|
|
2055
|
+
* const rates = await client.getOrderShippingRates('order_abc');
|
|
2056
|
+
* const cheapest = rates[0];
|
|
2057
|
+
* const label = await client.createShippingLabel('order_abc', { rateId: cheapest.id });
|
|
2058
|
+
* ```
|
|
2059
|
+
*/
|
|
2060
|
+
async getOrderShippingRates(orderId) {
|
|
2061
|
+
return this.request("GET", `/api/v1/orders/${encodePathSegment(orderId)}/shipments/app-rates`);
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* Shipments recorded for an order, each with its tracking history.
|
|
2065
|
+
*
|
|
2066
|
+
* History arrives on its own: the carrier's webhooks flow through the
|
|
2067
|
+
* installed shipping app and append events as the parcel moves. Poll this for
|
|
2068
|
+
* display if you need it; do not poll expecting to *drive* anything.
|
|
2069
|
+
*/
|
|
2070
|
+
async getOrderShipments(orderId) {
|
|
2071
|
+
return this.request("GET", `/api/v1/orders/${encodePathSegment(orderId)}/shipments`);
|
|
2072
|
+
}
|
|
1907
2073
|
/**
|
|
1908
2074
|
* Cancel an order
|
|
1909
2075
|
* Works for Shopify and WooCommerce orders that haven't been fulfilled
|
|
@@ -1918,16 +2084,31 @@ var BrainerceClient = class {
|
|
|
1918
2084
|
return this.request("POST", `/api/v1/orders/${encodePathSegment(orderId)}/cancel`);
|
|
1919
2085
|
}
|
|
1920
2086
|
/**
|
|
1921
|
-
* Fulfill an order (mark as shipped)
|
|
1922
|
-
*
|
|
2087
|
+
* Fulfill an order (mark as shipped), or correct the tracking of an order
|
|
2088
|
+
* that has already shipped.
|
|
2089
|
+
*
|
|
2090
|
+
* Pass `trackingUrl` alongside the number — the shipped email only renders
|
|
2091
|
+
* its "Track Your Order" button when a URL is present.
|
|
2092
|
+
*
|
|
2093
|
+
* Calling this again on an order that is already `SHIPPED`/`FULFILLED` with
|
|
2094
|
+
* tracking fields edits only those fields: the status does not move, the
|
|
2095
|
+
* ship date is not rewritten, and no fulfilment event fires. That is the way
|
|
2096
|
+
* to fix a mistyped tracking number.
|
|
1923
2097
|
*
|
|
1924
2098
|
* @example
|
|
1925
2099
|
* ```typescript
|
|
1926
|
-
*
|
|
2100
|
+
* // First fulfilment — emails the shopper by default.
|
|
2101
|
+
* await client.fulfillOrder('order_123', {
|
|
1927
2102
|
* trackingNumber: '1Z999AA10123456784',
|
|
1928
2103
|
* trackingCompany: 'UPS',
|
|
2104
|
+
* trackingUrl: 'https://www.ups.com/track?tracknum=1Z999AA10123456784',
|
|
1929
2105
|
* notifyCustomer: true,
|
|
1930
2106
|
* });
|
|
2107
|
+
*
|
|
2108
|
+
* // Correction — silent unless you opt back in.
|
|
2109
|
+
* await client.fulfillOrder('order_123', {
|
|
2110
|
+
* trackingNumber: '1Z999AA10123456785',
|
|
2111
|
+
* });
|
|
1931
2112
|
* ```
|
|
1932
2113
|
*/
|
|
1933
2114
|
async fulfillOrder(orderId, data) {
|
|
@@ -2668,11 +2849,25 @@ var BrainerceClient = class {
|
|
|
2668
2849
|
*
|
|
2669
2850
|
* @param provider - OAuth provider ('GOOGLE', 'FACEBOOK', 'GITHUB')
|
|
2670
2851
|
* @param options - Optional configuration
|
|
2671
|
-
* @param options.redirectUrl -
|
|
2672
|
-
*
|
|
2673
|
-
*
|
|
2674
|
-
*
|
|
2675
|
-
*
|
|
2852
|
+
* @param options.redirectUrl - Where to send the browser once OAuth finishes —
|
|
2853
|
+
* on success *and* on failure. Validated server-side against the sales
|
|
2854
|
+
* channel's trusted origins, so what is accepted depends on the mode:
|
|
2855
|
+
* - vibe-coded (`salesChannelId: 'vc_*'`): an absolute URL on the channel's
|
|
2856
|
+
* registered `domain` or one of its `allowedOrigins`; in TEST mode, any
|
|
2857
|
+
* `localhost`/`127.0.0.1` port. A relative path (`/auth/callback`) also
|
|
2858
|
+
* works — it is resolved against the channel's `domain` on the way back,
|
|
2859
|
+
* so the channel must have one registered.
|
|
2860
|
+
* - storefront (`storeId`): **social login cannot round-trip in this mode.**
|
|
2861
|
+
* No channel is bound to the request, so an absolute URL has no
|
|
2862
|
+
* trusted-origin list to match (400 at this call) and a relative path has
|
|
2863
|
+
* no origin to resolve against on the way back. Use a `salesChannelId`
|
|
2864
|
+
* connection for OAuth.
|
|
2865
|
+
* Anything invalid fails fast here, before the shopper ever reaches the
|
|
2866
|
+
* provider.
|
|
2867
|
+
*
|
|
2868
|
+
* @example
|
|
2869
|
+
* ```typescript
|
|
2870
|
+
* // Vibe-coded mode — absolute URL on the registered storefront domain
|
|
2676
2871
|
* const { authorizationUrl } = await client.getOAuthAuthorizeUrl('GOOGLE', {
|
|
2677
2872
|
* redirectUrl: window.location.origin + '/auth/callback'
|
|
2678
2873
|
* });
|
|
@@ -2689,7 +2884,17 @@ var BrainerceClient = class {
|
|
|
2689
2884
|
* client.setCustomerToken(result.token);
|
|
2690
2885
|
* // result.customer, result.isNewCustomer, result.redirectUrl, ...
|
|
2691
2886
|
* } else if (params.get('oauth_error')) {
|
|
2692
|
-
* //
|
|
2887
|
+
* // Failures land on this same page, on `redirectUrl` — never on the API
|
|
2888
|
+
* // host. `oauth_error` is a stable snake_case code (see OAuthErrorCode);
|
|
2889
|
+
* // `error_description` is English developer detail, not shopper copy.
|
|
2890
|
+
* const code = params.get('oauth_error') as OAuthErrorCode;
|
|
2891
|
+
* showMessage(
|
|
2892
|
+
* code === 'link_blocked_unverified_password_account'
|
|
2893
|
+
* ? t('auth.verifyEmailFirst') // send them to email verification
|
|
2894
|
+
* : code === 'state_expired'
|
|
2895
|
+
* ? t('auth.sessionExpiredRetry')
|
|
2896
|
+
* : t('auth.signInFailed')
|
|
2897
|
+
* );
|
|
2693
2898
|
* }
|
|
2694
2899
|
* ```
|
|
2695
2900
|
*/
|
|
@@ -3065,6 +3270,9 @@ var BrainerceClient = class {
|
|
|
3065
3270
|
* Create a new cart for a guest user
|
|
3066
3271
|
* Returns a cart with a sessionToken that identifies this cart
|
|
3067
3272
|
*
|
|
3273
|
+
* If `loadGoogleAnalytics()` was called and has resolved a GA4 client/session
|
|
3274
|
+
* id, it's auto-attached unless `options` already specifies one.
|
|
3275
|
+
*
|
|
3068
3276
|
* @example
|
|
3069
3277
|
* ```typescript
|
|
3070
3278
|
* const cart = await client.createCart();
|
|
@@ -3072,14 +3280,15 @@ var BrainerceClient = class {
|
|
|
3072
3280
|
* // Store sessionToken in localStorage or cookie
|
|
3073
3281
|
* ```
|
|
3074
3282
|
*/
|
|
3075
|
-
async createCart() {
|
|
3283
|
+
async createCart(options) {
|
|
3284
|
+
const body = await this.withAnalyticsStitchIds(options);
|
|
3076
3285
|
if (this.isVibeCodedMode()) {
|
|
3077
|
-
return this.vibeCodedRequest("POST", "/cart");
|
|
3286
|
+
return this.vibeCodedRequest("POST", "/cart", body);
|
|
3078
3287
|
}
|
|
3079
3288
|
if (this.storeId && !this.apiKey) {
|
|
3080
|
-
return this.storefrontRequest("POST", "/cart");
|
|
3289
|
+
return this.storefrontRequest("POST", "/cart", body);
|
|
3081
3290
|
}
|
|
3082
|
-
return this.adminRequest("POST", "/api/v1/cart");
|
|
3291
|
+
return this.adminRequest("POST", "/api/v1/cart", body);
|
|
3083
3292
|
}
|
|
3084
3293
|
/**
|
|
3085
3294
|
* Get a cart by session token (for guest users)
|
|
@@ -3228,20 +3437,21 @@ var BrainerceClient = class {
|
|
|
3228
3437
|
});
|
|
3229
3438
|
return this.withGuards(this.localCartToCart(this.getLocalCart()), "cart");
|
|
3230
3439
|
}
|
|
3440
|
+
const body = await this.withAnalyticsStitchIds(item);
|
|
3231
3441
|
if (this.isVibeCodedMode()) {
|
|
3232
3442
|
return this.withGuards(
|
|
3233
|
-
this.vibeCodedRequest("POST", `/cart/${encodePathSegment(cartId)}/items`,
|
|
3443
|
+
this.vibeCodedRequest("POST", `/cart/${encodePathSegment(cartId)}/items`, body),
|
|
3234
3444
|
"cart"
|
|
3235
3445
|
);
|
|
3236
3446
|
}
|
|
3237
3447
|
if (this.storeId && !this.apiKey) {
|
|
3238
3448
|
return this.withGuards(
|
|
3239
|
-
this.storefrontRequest("POST", `/cart/${encodePathSegment(cartId)}/items`,
|
|
3449
|
+
this.storefrontRequest("POST", `/cart/${encodePathSegment(cartId)}/items`, body),
|
|
3240
3450
|
"cart"
|
|
3241
3451
|
);
|
|
3242
3452
|
}
|
|
3243
3453
|
return this.withGuards(
|
|
3244
|
-
this.adminRequest("POST", `/api/v1/cart/${encodePathSegment(cartId)}/items`,
|
|
3454
|
+
this.adminRequest("POST", `/api/v1/cart/${encodePathSegment(cartId)}/items`, body),
|
|
3245
3455
|
"cart"
|
|
3246
3456
|
);
|
|
3247
3457
|
}
|
|
@@ -4833,24 +5043,25 @@ var BrainerceClient = class {
|
|
|
4833
5043
|
* ```
|
|
4834
5044
|
*/
|
|
4835
5045
|
async setCheckoutCustomer(checkoutId, data) {
|
|
5046
|
+
const body = await this.withAnalyticsStitchIds(data);
|
|
4836
5047
|
if (this.isVibeCodedMode()) {
|
|
4837
5048
|
return this.vibeCodedRequest(
|
|
4838
5049
|
"PATCH",
|
|
4839
5050
|
`/checkout/${encodePathSegment(checkoutId)}/customer`,
|
|
4840
|
-
|
|
5051
|
+
body
|
|
4841
5052
|
);
|
|
4842
5053
|
}
|
|
4843
5054
|
if (this.storeId && !this.apiKey) {
|
|
4844
5055
|
return this.storefrontRequest(
|
|
4845
5056
|
"PATCH",
|
|
4846
5057
|
`/checkout/${encodePathSegment(checkoutId)}/customer`,
|
|
4847
|
-
|
|
5058
|
+
body
|
|
4848
5059
|
);
|
|
4849
5060
|
}
|
|
4850
5061
|
return this.adminRequest(
|
|
4851
5062
|
"PATCH",
|
|
4852
5063
|
`/api/v1/checkout/${encodePathSegment(checkoutId)}/customer`,
|
|
4853
|
-
|
|
5064
|
+
body
|
|
4854
5065
|
);
|
|
4855
5066
|
}
|
|
4856
5067
|
/**
|
|
@@ -4904,6 +5115,12 @@ var BrainerceClient = class {
|
|
|
4904
5115
|
* should include an optional "Order notes" textarea by default and send its
|
|
4905
5116
|
* value here (or via `setCheckoutCustomer`). The note lands on the order.
|
|
4906
5117
|
*
|
|
5118
|
+
* **Pass `placeId` whenever the address came from `addressAutocomplete()`.**
|
|
5119
|
+
* The server re-resolves it to exact coordinates and matches polygon
|
|
5120
|
+
* ("draw on map") shipping zones against those instead of re-geocoding the
|
|
5121
|
+
* address text — which is materially less accurate and can place the
|
|
5122
|
+
* shopper in a neighbouring city's zone, or in none at all.
|
|
5123
|
+
*
|
|
4907
5124
|
* @example
|
|
4908
5125
|
* ```typescript
|
|
4909
5126
|
* const { checkout, rates } = await client.setShippingAddress('checkout_123', {
|
|
@@ -4916,29 +5133,32 @@ var BrainerceClient = class {
|
|
|
4916
5133
|
* postalCode: '10001',
|
|
4917
5134
|
* country: 'US',
|
|
4918
5135
|
* notes: 'Please leave the package at the door', // optional order notes
|
|
5136
|
+
* placeId: suggestion.placeId, // from addressAutocomplete()
|
|
5137
|
+
* placeSessionToken: sessionToken, // the same token used for it
|
|
4919
5138
|
* });
|
|
4920
5139
|
* console.log('Available rates:', rates);
|
|
4921
5140
|
* ```
|
|
4922
5141
|
*/
|
|
4923
5142
|
async setShippingAddress(checkoutId, address) {
|
|
5143
|
+
const body = await this.withAnalyticsStitchIds(address);
|
|
4924
5144
|
if (this.isVibeCodedMode()) {
|
|
4925
5145
|
return this.vibeCodedRequest(
|
|
4926
5146
|
"PATCH",
|
|
4927
5147
|
`/checkout/${encodePathSegment(checkoutId)}/shipping-address`,
|
|
4928
|
-
|
|
5148
|
+
body
|
|
4929
5149
|
);
|
|
4930
5150
|
}
|
|
4931
5151
|
if (this.storeId && !this.apiKey) {
|
|
4932
5152
|
return this.storefrontRequest(
|
|
4933
5153
|
"PATCH",
|
|
4934
5154
|
`/checkout/${encodePathSegment(checkoutId)}/shipping-address`,
|
|
4935
|
-
|
|
5155
|
+
body
|
|
4936
5156
|
);
|
|
4937
5157
|
}
|
|
4938
5158
|
return this.adminRequest(
|
|
4939
5159
|
"PATCH",
|
|
4940
5160
|
`/api/v1/checkout/${encodePathSegment(checkoutId)}/shipping-address`,
|
|
4941
|
-
|
|
5161
|
+
body
|
|
4942
5162
|
);
|
|
4943
5163
|
}
|
|
4944
5164
|
/**
|
|
@@ -8211,6 +8431,51 @@ var BrainerceClient = class {
|
|
|
8211
8431
|
}))
|
|
8212
8432
|
};
|
|
8213
8433
|
}
|
|
8434
|
+
/**
|
|
8435
|
+
* Get facet value counts for filterable metafield definitions — one entry
|
|
8436
|
+
* per definition the merchant marked `filterable: true` (types SELECT /
|
|
8437
|
+
* MULTI_SELECT / BOOLEAN), each with DISTINCT-product counts per value.
|
|
8438
|
+
* Powers faceted navigation ("Color: red (12) / blue (3)") without one
|
|
8439
|
+
* `getProducts` round trip per candidate value.
|
|
8440
|
+
*
|
|
8441
|
+
* Available in vibe-coded and storefront modes. On the vibe-coded surface
|
|
8442
|
+
* only definitions published to your connection are returned, and counts
|
|
8443
|
+
* reflect only products published to it.
|
|
8444
|
+
*
|
|
8445
|
+
* @example
|
|
8446
|
+
* ```typescript
|
|
8447
|
+
* const { filters } = await client.getMetafieldFilters();
|
|
8448
|
+
* for (const f of filters) {
|
|
8449
|
+
* // f.key pairs with getProducts({ metafields: { [f.key]: [value] } })
|
|
8450
|
+
* console.log(f.name, f.values); // [{ value: 'red', count: 12 }, ...]
|
|
8451
|
+
* }
|
|
8452
|
+
* ```
|
|
8453
|
+
*/
|
|
8454
|
+
async getMetafieldFilters(params) {
|
|
8455
|
+
const headerOverrides = params?.locale ? { "Accept-Language": params.locale } : void 0;
|
|
8456
|
+
if (this.isVibeCodedMode()) {
|
|
8457
|
+
return this.vibeCodedRequest(
|
|
8458
|
+
"GET",
|
|
8459
|
+
"/metafield-filters",
|
|
8460
|
+
void 0,
|
|
8461
|
+
void 0,
|
|
8462
|
+
headerOverrides
|
|
8463
|
+
);
|
|
8464
|
+
}
|
|
8465
|
+
if (this.storeId && !this.apiKey) {
|
|
8466
|
+
return this.storefrontRequest(
|
|
8467
|
+
"GET",
|
|
8468
|
+
"/metafield-filters",
|
|
8469
|
+
void 0,
|
|
8470
|
+
void 0,
|
|
8471
|
+
headerOverrides
|
|
8472
|
+
);
|
|
8473
|
+
}
|
|
8474
|
+
throw new BrainerceError(
|
|
8475
|
+
"getMetafieldFilters is only available in vibe-coded or storefront mode",
|
|
8476
|
+
400
|
|
8477
|
+
);
|
|
8478
|
+
}
|
|
8214
8479
|
/**
|
|
8215
8480
|
* Get all metafield definitions for the store
|
|
8216
8481
|
* Requires Admin mode (apiKey)
|
|
@@ -9203,6 +9468,11 @@ function formatAmount(amount, currency, locale) {
|
|
|
9203
9468
|
// src/date-availability.ts
|
|
9204
9469
|
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
9205
9470
|
var TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
|
|
9471
|
+
var DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
9472
|
+
var DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,9}))?(Z|z|[+-]\d{2}:\d{2})?$/;
|
|
9473
|
+
var MIN_OFFSET_MINUTES = -12 * 60;
|
|
9474
|
+
var MAX_OFFSET_MINUTES = 14 * 60;
|
|
9475
|
+
var VALID_OFFSET_MINUTE_PARTS = [0, 30, 45];
|
|
9206
9476
|
function validateDateAvailabilityConfig(config, fieldType) {
|
|
9207
9477
|
const errors = [];
|
|
9208
9478
|
if (!config) return errors;
|
|
@@ -9303,6 +9573,101 @@ function resolveStoreLocalParts(instant, timezone) {
|
|
|
9303
9573
|
weekday: WEEKDAY_TO_INDEX[get("weekday")] ?? instant.getUTCDay()
|
|
9304
9574
|
};
|
|
9305
9575
|
}
|
|
9576
|
+
function parseDateFieldValue(raw, fieldType, timezone) {
|
|
9577
|
+
const str = raw instanceof Date ? Number.isNaN(raw.getTime()) ? "" : raw.toISOString() : typeof raw === "string" ? raw.trim() : "";
|
|
9578
|
+
const expected = fieldType === "DATE" ? "expected a calendar date in YYYY-MM-DD format" : "expected an ISO-8601 date/time such as 2026-08-13T13:00:00+03:00";
|
|
9579
|
+
if (!str) return { status: "invalid", reason: `"${String(raw)}" is not a valid date \u2014 ${expected}` };
|
|
9580
|
+
const dateOnly = DATE_ONLY_RE.exec(str);
|
|
9581
|
+
const dateTime = dateOnly ? null : DATE_TIME_RE.exec(str);
|
|
9582
|
+
const match = dateOnly ?? dateTime;
|
|
9583
|
+
if (!match) return { status: "invalid", reason: `"${str}" is not a valid date \u2014 ${expected}` };
|
|
9584
|
+
const year = Number(match[1]);
|
|
9585
|
+
const month = Number(match[2]);
|
|
9586
|
+
const day = Number(match[3]);
|
|
9587
|
+
if (!isRealCalendarDate(year, month, day)) {
|
|
9588
|
+
return { status: "invalid", reason: `"${str}" is not a real calendar date` };
|
|
9589
|
+
}
|
|
9590
|
+
const dateYYYYMMDD = `${match[1]}-${match[2]}-${match[3]}`;
|
|
9591
|
+
if (fieldType === "DATE") {
|
|
9592
|
+
return {
|
|
9593
|
+
status: "valid",
|
|
9594
|
+
value: { instant: /* @__PURE__ */ new Date(`${dateYYYYMMDD}T00:00:00.000Z`), normalized: dateYYYYMMDD }
|
|
9595
|
+
};
|
|
9596
|
+
}
|
|
9597
|
+
if (!dateTime) {
|
|
9598
|
+
const instant2 = instantFromStoreLocal(dateYYYYMMDD, 0, 0, 0, 0, timezone);
|
|
9599
|
+
return { status: "valid", value: { instant: instant2, normalized: instant2.toISOString() } };
|
|
9600
|
+
}
|
|
9601
|
+
const hour = Number(dateTime[4]);
|
|
9602
|
+
const minute = Number(dateTime[5]);
|
|
9603
|
+
const second = Number(dateTime[6] ?? "0");
|
|
9604
|
+
const millis = Number((dateTime[7] ?? "").slice(0, 3).padEnd(3, "0") || "0");
|
|
9605
|
+
if (hour > 23 || minute > 59 || second > 59) {
|
|
9606
|
+
return { status: "invalid", reason: `"${str}" has an out-of-range time` };
|
|
9607
|
+
}
|
|
9608
|
+
const offset = dateTime[8];
|
|
9609
|
+
if (!offset) {
|
|
9610
|
+
const instant2 = instantFromStoreLocal(dateYYYYMMDD, hour, minute, second, millis, timezone);
|
|
9611
|
+
return { status: "valid", value: { instant: instant2, normalized: instant2.toISOString() } };
|
|
9612
|
+
}
|
|
9613
|
+
let offsetMinutes = 0;
|
|
9614
|
+
if (offset !== "Z" && offset !== "z") {
|
|
9615
|
+
const offsetHourPart = Number(offset.slice(1, 3));
|
|
9616
|
+
const offsetMinutePart = Number(offset.slice(4, 6));
|
|
9617
|
+
offsetMinutes = (offset[0] === "-" ? -1 : 1) * (offsetHourPart * 60 + offsetMinutePart);
|
|
9618
|
+
if (offsetMinutes < MIN_OFFSET_MINUTES || offsetMinutes > MAX_OFFSET_MINUTES) {
|
|
9619
|
+
return {
|
|
9620
|
+
status: "invalid",
|
|
9621
|
+
reason: `"${str}" ends in "${offset}", which is not a real UTC offset (they run -12:00 to +14:00). A time-slot label such as "13:00-14:00" is not an offset \u2014 ${expected}`
|
|
9622
|
+
};
|
|
9623
|
+
}
|
|
9624
|
+
if (!VALID_OFFSET_MINUTE_PARTS.includes(offsetMinutePart)) {
|
|
9625
|
+
return {
|
|
9626
|
+
status: "invalid",
|
|
9627
|
+
reason: `"${str}" ends in "${offset}", which is not a real UTC offset (the minutes are always :00, :30 or :45). A time-slot label such as "13:00-14:00" is not an offset \u2014 ${expected}`
|
|
9628
|
+
};
|
|
9629
|
+
}
|
|
9630
|
+
}
|
|
9631
|
+
const instant = new Date(
|
|
9632
|
+
Date.UTC(year, month - 1, day, hour, minute, second, millis) - offsetMinutes * 6e4
|
|
9633
|
+
);
|
|
9634
|
+
return { status: "valid", value: { instant, normalized: instant.toISOString() } };
|
|
9635
|
+
}
|
|
9636
|
+
function isRealCalendarDate(year, month, day) {
|
|
9637
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) return false;
|
|
9638
|
+
const probe = new Date(Date.UTC(year, month - 1, day));
|
|
9639
|
+
return probe.getUTCFullYear() === year && probe.getUTCMonth() === month - 1 && probe.getUTCDate() === day;
|
|
9640
|
+
}
|
|
9641
|
+
function instantFromStoreLocal(dateYYYYMMDD, hour, minute, second, millis, timezone) {
|
|
9642
|
+
const [year, month, day] = dateYYYYMMDD.split("-").map(Number);
|
|
9643
|
+
const naive = Date.UTC(year, month - 1, day, hour, minute, second, millis);
|
|
9644
|
+
const firstOffset = timezoneOffsetMs(naive, timezone);
|
|
9645
|
+
let instant = naive - firstOffset;
|
|
9646
|
+
const secondOffset = timezoneOffsetMs(instant, timezone);
|
|
9647
|
+
if (secondOffset !== firstOffset) instant = naive - secondOffset;
|
|
9648
|
+
return new Date(instant);
|
|
9649
|
+
}
|
|
9650
|
+
function timezoneOffsetMs(utcMillis, timezone) {
|
|
9651
|
+
let parts;
|
|
9652
|
+
try {
|
|
9653
|
+
parts = new Intl.DateTimeFormat("en-US", {
|
|
9654
|
+
timeZone: timezone,
|
|
9655
|
+
year: "numeric",
|
|
9656
|
+
month: "2-digit",
|
|
9657
|
+
day: "2-digit",
|
|
9658
|
+
hour: "2-digit",
|
|
9659
|
+
minute: "2-digit",
|
|
9660
|
+
second: "2-digit",
|
|
9661
|
+
hour12: false
|
|
9662
|
+
}).formatToParts(new Date(utcMillis));
|
|
9663
|
+
} catch {
|
|
9664
|
+
return 0;
|
|
9665
|
+
}
|
|
9666
|
+
const get = (type) => Number(parts.find((p) => p.type === type)?.value ?? "0");
|
|
9667
|
+
const hour = get("hour") === 24 ? 0 : get("hour");
|
|
9668
|
+
const asIfUtc = Date.UTC(get("year"), get("month") - 1, get("day"), hour, get("minute"), get("second"));
|
|
9669
|
+
return asIfUtc - Math.floor(utcMillis / 1e3) * 1e3;
|
|
9670
|
+
}
|
|
9306
9671
|
function isCalendarDateAllowed(dateYYYYMMDD, config) {
|
|
9307
9672
|
if (!config) return true;
|
|
9308
9673
|
if (config.minDate && dateYYYYMMDD < config.minDate) return false;
|
|
@@ -9330,6 +9695,12 @@ function computeAvailableSlots(config, dateYYYYMMDD) {
|
|
|
9330
9695
|
}
|
|
9331
9696
|
return slots;
|
|
9332
9697
|
}
|
|
9698
|
+
function getBusinessHoursForDate(config, dateYYYYMMDD) {
|
|
9699
|
+
if (!config?.businessHours?.length) return [];
|
|
9700
|
+
if (!isCalendarDateAllowed(dateYYYYMMDD, config)) return [];
|
|
9701
|
+
const weekday = weekdayOfDateString(dateYYYYMMDD);
|
|
9702
|
+
return config.businessHours.filter((w) => w.weekday === weekday);
|
|
9703
|
+
}
|
|
9333
9704
|
function isDateValueAllowed(instant, config, fieldType, timezone) {
|
|
9334
9705
|
if (!config) return { allowed: true };
|
|
9335
9706
|
if (fieldType === "DATE") {
|
|
@@ -9879,6 +10250,7 @@ function isCouponApplicableToProduct(coupon, productId) {
|
|
|
9879
10250
|
formatProductPrice,
|
|
9880
10251
|
formatVariantPrice,
|
|
9881
10252
|
getBlogSitemapEntries,
|
|
10253
|
+
getBusinessHoursForDate,
|
|
9882
10254
|
getCartItemImage,
|
|
9883
10255
|
getCartItemName,
|
|
9884
10256
|
getCartTotals,
|
|
@@ -9903,6 +10275,7 @@ function isCouponApplicableToProduct(coupon, productId) {
|
|
|
9903
10275
|
isHtmlDescription,
|
|
9904
10276
|
isWebhookEventType,
|
|
9905
10277
|
jsonLdScriptProps,
|
|
10278
|
+
parseDateFieldValue,
|
|
9906
10279
|
parseWebhookEvent,
|
|
9907
10280
|
resolveStoreLocalParts,
|
|
9908
10281
|
safePaymentRedirect,
|