brainerce 1.51.0 → 1.53.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 +350 -47
- 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 +494 -14
- package/dist/index.d.ts +494 -14
- package/dist/index.js +434 -31
- package/dist/index.mjs +432 -31
- 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.53.0";
|
|
203
205
|
|
|
204
206
|
// src/client.ts
|
|
205
207
|
var DEFAULT_BASE_URL = "https://api.brainerce.com";
|
|
@@ -239,7 +241,7 @@ function parseRetryAfterMs(response) {
|
|
|
239
241
|
function sleep(ms) {
|
|
240
242
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
241
243
|
}
|
|
242
|
-
var
|
|
244
|
+
var _BrainerceClient = class _BrainerceClient {
|
|
243
245
|
constructor(options) {
|
|
244
246
|
this.customerToken = null;
|
|
245
247
|
this.customerCartId = null;
|
|
@@ -255,6 +257,11 @@ 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;
|
|
263
|
+
/** One warning per client, not per keystroke-driven address submit. */
|
|
264
|
+
this._warnedResolvedOnlyAddressFields = false;
|
|
258
265
|
/** localStorage key for session cart reference (sessionToken + cartId) */
|
|
259
266
|
this.SESSION_CART_KEY = "brainerce_session";
|
|
260
267
|
/**
|
|
@@ -768,10 +775,19 @@ var BrainerceClient = class {
|
|
|
768
775
|
* Set the customer authentication token (obtained from login/register).
|
|
769
776
|
* Required for accessing customer-specific data in storefront mode.
|
|
770
777
|
*
|
|
778
|
+
* This is a plain setter — it authenticates subsequent requests and nothing
|
|
779
|
+
* else. In particular it does NOT attach the shopper's existing guest cart to
|
|
780
|
+
* their account. Pair every sign-in with {@link syncCartOnLogin}, or the cart
|
|
781
|
+
* stays anonymous and every feature keyed on buyer identity degrades quietly:
|
|
782
|
+
* "first order only" discounts keep applying to returning customers,
|
|
783
|
+
* per-customer usage caps stop being enforced at cart time, and abandoned-cart
|
|
784
|
+
* recovery can't identify who to email.
|
|
785
|
+
*
|
|
771
786
|
* @example
|
|
772
787
|
* ```typescript
|
|
773
788
|
* const auth = await client.loginCustomer('user@example.com', 'password');
|
|
774
789
|
* client.setCustomerToken(auth.token);
|
|
790
|
+
* await client.syncCartOnLogin(); // claim the guest cart for this account
|
|
775
791
|
*
|
|
776
792
|
* // Now can access customer data
|
|
777
793
|
* const profile = await client.getMyProfile();
|
|
@@ -1165,6 +1181,159 @@ var BrainerceClient = class {
|
|
|
1165
1181
|
} catch {
|
|
1166
1182
|
}
|
|
1167
1183
|
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Load GA4's `gtag.js` and start resolving the `client_id`/`session_id`
|
|
1186
|
+
* "stitch" ids Google needs to join a later server-side purchase conversion
|
|
1187
|
+
* to this browser's GA4 session (without them, a server-sent purchase event
|
|
1188
|
+
* either gets rejected or creates a phantom user in GA4). Call this once,
|
|
1189
|
+
* as early as possible (app entry / root layout).
|
|
1190
|
+
*
|
|
1191
|
+
* `createCart()`, `addToCart()`, `setCheckoutCustomer()`, and
|
|
1192
|
+
* `setShippingAddress()` all auto-attach the resolved ids once available —
|
|
1193
|
+
* an explicit `analyticsClientId`/`analyticsSessionId` you pass to any of
|
|
1194
|
+
* those always wins over the auto-captured value. You only need to call
|
|
1195
|
+
* this once; every subsequent cart/checkout call benefits automatically.
|
|
1196
|
+
*
|
|
1197
|
+
* Ids are resolved via `gtag('get', measurementId, 'client_id' | 'session_id', cb)`
|
|
1198
|
+
* — Google's documented method — never by parsing the `_ga` cookie, which
|
|
1199
|
+
* is undocumented and breaks silently across cookie-format changes and
|
|
1200
|
+
* Consent Mode v2 states. If the shopper has denied analytics consent,
|
|
1201
|
+
* gtag reports no client_id and the ids are simply omitted — never
|
|
1202
|
+
* synthesized.
|
|
1203
|
+
*
|
|
1204
|
+
* No-op outside the browser (SSR-safe) and never throws — a blocked or
|
|
1205
|
+
* slow gtag just means server-side conversions won't stitch; it never
|
|
1206
|
+
* breaks the storefront or delays checkout by more than `options.timeoutMs`.
|
|
1207
|
+
*
|
|
1208
|
+
* @example
|
|
1209
|
+
* ```typescript
|
|
1210
|
+
* // Call once, e.g. in your root layout / app entry point
|
|
1211
|
+
* client.loadGoogleAnalytics('G-XXXXXXX');
|
|
1212
|
+
*
|
|
1213
|
+
* // Every cart/checkout call from here on auto-forwards the stitch ids —
|
|
1214
|
+
* // no other code changes needed.
|
|
1215
|
+
* const cart = await client.createCart();
|
|
1216
|
+
* await client.addToCart(cart.id, { productId: 'prod_abc', quantity: 1 });
|
|
1217
|
+
* ```
|
|
1218
|
+
*/
|
|
1219
|
+
loadGoogleAnalytics(measurementId, options) {
|
|
1220
|
+
if (typeof window === "undefined" || !measurementId) return;
|
|
1221
|
+
if (this._ga4MeasurementId === measurementId && this._ga4StitchPromise) return;
|
|
1222
|
+
this._ga4MeasurementId = measurementId;
|
|
1223
|
+
try {
|
|
1224
|
+
if (!window.gtag) {
|
|
1225
|
+
window.dataLayer = window.dataLayer || [];
|
|
1226
|
+
const gtag = (...args) => {
|
|
1227
|
+
window.dataLayer.push(args);
|
|
1228
|
+
};
|
|
1229
|
+
window.gtag = gtag;
|
|
1230
|
+
gtag("js", /* @__PURE__ */ new Date());
|
|
1231
|
+
const script = document.createElement("script");
|
|
1232
|
+
script.async = true;
|
|
1233
|
+
script.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`;
|
|
1234
|
+
document.head.appendChild(script);
|
|
1235
|
+
}
|
|
1236
|
+
window.gtag("config", measurementId);
|
|
1237
|
+
} catch {
|
|
1238
|
+
}
|
|
1239
|
+
this._ga4StitchPromise = this.resolveGa4StitchIds(measurementId, options?.timeoutMs ?? 1500);
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Resolve GA4's `client_id`/`session_id` via `gtag('get', ...)`, bounded by
|
|
1243
|
+
* `timeoutMs` so a slow/blocked gtag never hangs a cart/checkout call.
|
|
1244
|
+
* Resolves to `{}` (never rejects) on timeout, missing gtag, or denied
|
|
1245
|
+
* consent.
|
|
1246
|
+
*/
|
|
1247
|
+
resolveGa4StitchIds(measurementId, timeoutMs) {
|
|
1248
|
+
return new Promise((resolve) => {
|
|
1249
|
+
if (typeof window === "undefined" || !window.gtag) {
|
|
1250
|
+
resolve({});
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
const result = {};
|
|
1254
|
+
let settled = false;
|
|
1255
|
+
const finish = () => {
|
|
1256
|
+
if (settled) return;
|
|
1257
|
+
settled = true;
|
|
1258
|
+
resolve(result);
|
|
1259
|
+
};
|
|
1260
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
1261
|
+
let pending = 2;
|
|
1262
|
+
const done = () => {
|
|
1263
|
+
pending -= 1;
|
|
1264
|
+
if (pending === 0) {
|
|
1265
|
+
clearTimeout(timer);
|
|
1266
|
+
finish();
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
try {
|
|
1270
|
+
window.gtag("get", measurementId, "client_id", (id) => {
|
|
1271
|
+
if (id) result.analyticsClientId = id;
|
|
1272
|
+
done();
|
|
1273
|
+
});
|
|
1274
|
+
window.gtag("get", measurementId, "session_id", (id) => {
|
|
1275
|
+
if (id) result.analyticsSessionId = String(id);
|
|
1276
|
+
done();
|
|
1277
|
+
});
|
|
1278
|
+
} catch {
|
|
1279
|
+
clearTimeout(timer);
|
|
1280
|
+
finish();
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Merge the resolved GA4 stitch ids onto a request body — only for fields
|
|
1286
|
+
* the caller didn't already set explicitly (explicit values always win).
|
|
1287
|
+
* No-op (returns `dto` unchanged) if `loadGoogleAnalytics()` was never
|
|
1288
|
+
* called, or if it hasn't resolved any ids by the time this is awaited.
|
|
1289
|
+
*/
|
|
1290
|
+
async withAnalyticsStitchIds(dto) {
|
|
1291
|
+
if (!this._ga4StitchPromise) return dto;
|
|
1292
|
+
try {
|
|
1293
|
+
const ids = await this._ga4StitchPromise;
|
|
1294
|
+
if (!ids.analyticsClientId && !ids.analyticsSessionId) return dto;
|
|
1295
|
+
return {
|
|
1296
|
+
...dto ?? {},
|
|
1297
|
+
analyticsClientId: dto?.analyticsClientId ?? ids.analyticsClientId,
|
|
1298
|
+
analyticsSessionId: dto?.analyticsSessionId ?? ids.analyticsSessionId
|
|
1299
|
+
};
|
|
1300
|
+
} catch {
|
|
1301
|
+
return dto;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Drop the fields `getAddressDetails()` returns that no address endpoint
|
|
1306
|
+
* accepts, so spreading its `address` straight into `setShippingAddress()`
|
|
1307
|
+
* / `setBillingAddress()` works instead of failing the whole request.
|
|
1308
|
+
*
|
|
1309
|
+
* The address endpoints validate against a strict allow-list: ONE unknown
|
|
1310
|
+
* property rejects the call with `400 "property lat should not exist"`, and
|
|
1311
|
+
* the shopper cannot check out at all. `lat`/`lng`/`formattedAddress` are
|
|
1312
|
+
* the only realistic way to hit that — they come out of this SDK's own
|
|
1313
|
+
* resolved-address shape, so this SDK cleans up after itself rather than
|
|
1314
|
+
* making every storefront remember to. Nothing else is stripped: a genuine
|
|
1315
|
+
* typo still reaches the server and still fails loudly.
|
|
1316
|
+
*
|
|
1317
|
+
* Coordinates are dropped rather than forwarded because zone matching picks
|
|
1318
|
+
* which shipping rate is offered and charged — the server resolves them
|
|
1319
|
+
* itself from `placeId`, and never takes them from the caller.
|
|
1320
|
+
*/
|
|
1321
|
+
stripResolvedOnlyAddressFields(address) {
|
|
1322
|
+
if (!address || typeof address !== "object") return address;
|
|
1323
|
+
const present = _BrainerceClient.RESOLVED_ONLY_ADDRESS_FIELDS.filter(
|
|
1324
|
+
(field) => field in address
|
|
1325
|
+
);
|
|
1326
|
+
if (present.length === 0) return address;
|
|
1327
|
+
const cleaned = { ...address };
|
|
1328
|
+
for (const field of present) delete cleaned[field];
|
|
1329
|
+
if (!this._warnedResolvedOnlyAddressFields) {
|
|
1330
|
+
this._warnedResolvedOnlyAddressFields = true;
|
|
1331
|
+
console.warn(
|
|
1332
|
+
`BrainerceClient: dropped ${present.join("/")} from the address payload \u2014 the API does not accept coordinates from the client. Pass \`placeId\` (and \`placeSessionToken\`) instead so the server resolves them itself and matches map-drawn delivery zones against the exact location.`
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
return cleaned;
|
|
1336
|
+
}
|
|
1168
1337
|
// -------------------- Products --------------------
|
|
1169
1338
|
/**
|
|
1170
1339
|
* Get a list of products with pagination and filtering
|
|
@@ -1885,13 +2054,23 @@ var BrainerceClient = class {
|
|
|
1885
2054
|
}
|
|
1886
2055
|
/**
|
|
1887
2056
|
* 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
|
|
2057
|
+
* Pass the rate ID returned from rate shopping — treat it as opaque and never
|
|
2058
|
+
* parse it. Billing goes directly to the merchant's carrier account; Brainerce
|
|
2059
|
+
* is not a billing intermediary.
|
|
2060
|
+
*
|
|
2061
|
+
* `labelFormat` defaults to `PDF`. Use `ZPL` or `EPL` for warehouse thermal
|
|
2062
|
+
* printers. If the carrier cannot produce the requested format it returns its
|
|
2063
|
+
* closest match rather than failing the purchase — check the response.
|
|
2064
|
+
*
|
|
2065
|
+
* Once the label exists, tracking updates arrive automatically: the carrier's
|
|
2066
|
+
* webhooks move the shipment through in-transit → delivered and complete the
|
|
2067
|
+
* order. No polling required.
|
|
1890
2068
|
*
|
|
1891
2069
|
* @example
|
|
1892
2070
|
* ```typescript
|
|
1893
2071
|
* const label = await client.createShippingLabel('order_abc', {
|
|
1894
2072
|
* rateId: 'rate_8f123456789abcdef',
|
|
2073
|
+
* labelFormat: 'ZPL',
|
|
1895
2074
|
* });
|
|
1896
2075
|
* console.log('Label URL:', label.labelUrl);
|
|
1897
2076
|
* console.log('Tracking:', label.trackingNumber);
|
|
@@ -1904,6 +2083,37 @@ var BrainerceClient = class {
|
|
|
1904
2083
|
data
|
|
1905
2084
|
);
|
|
1906
2085
|
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Live carrier rates for an order, from the merchant's installed shipping app.
|
|
2088
|
+
*
|
|
2089
|
+
* Call this immediately before {@link createShippingLabel} and pass the chosen
|
|
2090
|
+
* `id` straight through — the id is opaque and must not be parsed. It is also
|
|
2091
|
+
* short-lived: this call is what creates the shipment at the carrier, so a
|
|
2092
|
+
* rate from an old call may no longer be purchasable.
|
|
2093
|
+
*
|
|
2094
|
+
* Returns `[]` when the store has no shipping app installed, or when the
|
|
2095
|
+
* order has no usable shipping address.
|
|
2096
|
+
*
|
|
2097
|
+
* @example
|
|
2098
|
+
* ```typescript
|
|
2099
|
+
* const rates = await client.getOrderShippingRates('order_abc');
|
|
2100
|
+
* const cheapest = rates[0];
|
|
2101
|
+
* const label = await client.createShippingLabel('order_abc', { rateId: cheapest.id });
|
|
2102
|
+
* ```
|
|
2103
|
+
*/
|
|
2104
|
+
async getOrderShippingRates(orderId) {
|
|
2105
|
+
return this.request("GET", `/api/v1/orders/${encodePathSegment(orderId)}/shipments/app-rates`);
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Shipments recorded for an order, each with its tracking history.
|
|
2109
|
+
*
|
|
2110
|
+
* History arrives on its own: the carrier's webhooks flow through the
|
|
2111
|
+
* installed shipping app and append events as the parcel moves. Poll this for
|
|
2112
|
+
* display if you need it; do not poll expecting to *drive* anything.
|
|
2113
|
+
*/
|
|
2114
|
+
async getOrderShipments(orderId) {
|
|
2115
|
+
return this.request("GET", `/api/v1/orders/${encodePathSegment(orderId)}/shipments`);
|
|
2116
|
+
}
|
|
1907
2117
|
/**
|
|
1908
2118
|
* Cancel an order
|
|
1909
2119
|
* Works for Shopify and WooCommerce orders that haven't been fulfilled
|
|
@@ -1918,16 +2128,31 @@ var BrainerceClient = class {
|
|
|
1918
2128
|
return this.request("POST", `/api/v1/orders/${encodePathSegment(orderId)}/cancel`);
|
|
1919
2129
|
}
|
|
1920
2130
|
/**
|
|
1921
|
-
* Fulfill an order (mark as shipped)
|
|
1922
|
-
*
|
|
2131
|
+
* Fulfill an order (mark as shipped), or correct the tracking of an order
|
|
2132
|
+
* that has already shipped.
|
|
2133
|
+
*
|
|
2134
|
+
* Pass `trackingUrl` alongside the number — the shipped email only renders
|
|
2135
|
+
* its "Track Your Order" button when a URL is present.
|
|
2136
|
+
*
|
|
2137
|
+
* Calling this again on an order that is already `SHIPPED`/`FULFILLED` with
|
|
2138
|
+
* tracking fields edits only those fields: the status does not move, the
|
|
2139
|
+
* ship date is not rewritten, and no fulfilment event fires. That is the way
|
|
2140
|
+
* to fix a mistyped tracking number.
|
|
1923
2141
|
*
|
|
1924
2142
|
* @example
|
|
1925
2143
|
* ```typescript
|
|
1926
|
-
*
|
|
2144
|
+
* // First fulfilment — emails the shopper by default.
|
|
2145
|
+
* await client.fulfillOrder('order_123', {
|
|
1927
2146
|
* trackingNumber: '1Z999AA10123456784',
|
|
1928
2147
|
* trackingCompany: 'UPS',
|
|
2148
|
+
* trackingUrl: 'https://www.ups.com/track?tracknum=1Z999AA10123456784',
|
|
1929
2149
|
* notifyCustomer: true,
|
|
1930
2150
|
* });
|
|
2151
|
+
*
|
|
2152
|
+
* // Correction — silent unless you opt back in.
|
|
2153
|
+
* await client.fulfillOrder('order_123', {
|
|
2154
|
+
* trackingNumber: '1Z999AA10123456785',
|
|
2155
|
+
* });
|
|
1931
2156
|
* ```
|
|
1932
2157
|
*/
|
|
1933
2158
|
async fulfillOrder(orderId, data) {
|
|
@@ -2668,11 +2893,25 @@ var BrainerceClient = class {
|
|
|
2668
2893
|
*
|
|
2669
2894
|
* @param provider - OAuth provider ('GOOGLE', 'FACEBOOK', 'GITHUB')
|
|
2670
2895
|
* @param options - Optional configuration
|
|
2671
|
-
* @param options.redirectUrl -
|
|
2672
|
-
*
|
|
2673
|
-
*
|
|
2674
|
-
*
|
|
2675
|
-
*
|
|
2896
|
+
* @param options.redirectUrl - Where to send the browser once OAuth finishes —
|
|
2897
|
+
* on success *and* on failure. Validated server-side against the sales
|
|
2898
|
+
* channel's trusted origins, so what is accepted depends on the mode:
|
|
2899
|
+
* - vibe-coded (`salesChannelId: 'vc_*'`): an absolute URL on the channel's
|
|
2900
|
+
* registered `domain` or one of its `allowedOrigins`; in TEST mode, any
|
|
2901
|
+
* `localhost`/`127.0.0.1` port. A relative path (`/auth/callback`) also
|
|
2902
|
+
* works — it is resolved against the channel's `domain` on the way back,
|
|
2903
|
+
* so the channel must have one registered.
|
|
2904
|
+
* - storefront (`storeId`): **social login cannot round-trip in this mode.**
|
|
2905
|
+
* No channel is bound to the request, so an absolute URL has no
|
|
2906
|
+
* trusted-origin list to match (400 at this call) and a relative path has
|
|
2907
|
+
* no origin to resolve against on the way back. Use a `salesChannelId`
|
|
2908
|
+
* connection for OAuth.
|
|
2909
|
+
* Anything invalid fails fast here, before the shopper ever reaches the
|
|
2910
|
+
* provider.
|
|
2911
|
+
*
|
|
2912
|
+
* @example
|
|
2913
|
+
* ```typescript
|
|
2914
|
+
* // Vibe-coded mode — absolute URL on the registered storefront domain
|
|
2676
2915
|
* const { authorizationUrl } = await client.getOAuthAuthorizeUrl('GOOGLE', {
|
|
2677
2916
|
* redirectUrl: window.location.origin + '/auth/callback'
|
|
2678
2917
|
* });
|
|
@@ -2687,9 +2926,25 @@ var BrainerceClient = class {
|
|
|
2687
2926
|
* if (params.get('oauth_success') === 'true' && params.get('auth_code')) {
|
|
2688
2927
|
* const result = await client.exchangeOAuthCode(params.get('auth_code')!);
|
|
2689
2928
|
* client.setCustomerToken(result.token);
|
|
2929
|
+
* // REQUIRED: setCustomerToken only stores the JWT — it does NOT attach the
|
|
2930
|
+
* // guest cart to the account. Without this call the cart stays anonymous,
|
|
2931
|
+
* // and anything keyed on the buyer's identity misbehaves: "first order
|
|
2932
|
+
* // only" discounts re-apply to returning customers, per-customer usage
|
|
2933
|
+
* // caps go unenforced, and abandoned-cart recovery can't reach them.
|
|
2934
|
+
* await client.syncCartOnLogin();
|
|
2690
2935
|
* // result.customer, result.isNewCustomer, result.redirectUrl, ...
|
|
2691
2936
|
* } else if (params.get('oauth_error')) {
|
|
2692
|
-
* //
|
|
2937
|
+
* // Failures land on this same page, on `redirectUrl` — never on the API
|
|
2938
|
+
* // host. `oauth_error` is a stable snake_case code (see OAuthErrorCode);
|
|
2939
|
+
* // `error_description` is English developer detail, not shopper copy.
|
|
2940
|
+
* const code = params.get('oauth_error') as OAuthErrorCode;
|
|
2941
|
+
* showMessage(
|
|
2942
|
+
* code === 'link_blocked_unverified_password_account'
|
|
2943
|
+
* ? t('auth.verifyEmailFirst') // send them to email verification
|
|
2944
|
+
* : code === 'state_expired'
|
|
2945
|
+
* ? t('auth.sessionExpiredRetry')
|
|
2946
|
+
* : t('auth.signInFailed')
|
|
2947
|
+
* );
|
|
2693
2948
|
* }
|
|
2694
2949
|
* ```
|
|
2695
2950
|
*/
|
|
@@ -2729,6 +2984,11 @@ var BrainerceClient = class {
|
|
|
2729
2984
|
*
|
|
2730
2985
|
* @param authCode - The single-use code from the `?auth_code=` URL param.
|
|
2731
2986
|
*
|
|
2987
|
+
* Always follow a successful exchange with `syncCartOnLogin()`. Storing the
|
|
2988
|
+
* token does not claim the guest cart, and an unclaimed cart has no buyer
|
|
2989
|
+
* identity — which silently breaks first-order discounts, per-customer usage
|
|
2990
|
+
* caps, and abandoned-cart recovery for everyone who signs in with OAuth.
|
|
2991
|
+
*
|
|
2732
2992
|
* @example
|
|
2733
2993
|
* ```typescript
|
|
2734
2994
|
* const params = new URLSearchParams(window.location.search);
|
|
@@ -2737,6 +2997,7 @@ var BrainerceClient = class {
|
|
|
2737
2997
|
* const { token, customer, isNewCustomer, redirectUrl } =
|
|
2738
2998
|
* await client.exchangeOAuthCode(code);
|
|
2739
2999
|
* client.setCustomerToken(token);
|
|
3000
|
+
* await client.syncCartOnLogin(); // attach the guest cart to the account
|
|
2740
3001
|
* }
|
|
2741
3002
|
* ```
|
|
2742
3003
|
*/
|
|
@@ -3065,6 +3326,9 @@ var BrainerceClient = class {
|
|
|
3065
3326
|
* Create a new cart for a guest user
|
|
3066
3327
|
* Returns a cart with a sessionToken that identifies this cart
|
|
3067
3328
|
*
|
|
3329
|
+
* If `loadGoogleAnalytics()` was called and has resolved a GA4 client/session
|
|
3330
|
+
* id, it's auto-attached unless `options` already specifies one.
|
|
3331
|
+
*
|
|
3068
3332
|
* @example
|
|
3069
3333
|
* ```typescript
|
|
3070
3334
|
* const cart = await client.createCart();
|
|
@@ -3072,14 +3336,15 @@ var BrainerceClient = class {
|
|
|
3072
3336
|
* // Store sessionToken in localStorage or cookie
|
|
3073
3337
|
* ```
|
|
3074
3338
|
*/
|
|
3075
|
-
async createCart() {
|
|
3339
|
+
async createCart(options) {
|
|
3340
|
+
const body = await this.withAnalyticsStitchIds(options);
|
|
3076
3341
|
if (this.isVibeCodedMode()) {
|
|
3077
|
-
return this.vibeCodedRequest("POST", "/cart");
|
|
3342
|
+
return this.vibeCodedRequest("POST", "/cart", body);
|
|
3078
3343
|
}
|
|
3079
3344
|
if (this.storeId && !this.apiKey) {
|
|
3080
|
-
return this.storefrontRequest("POST", "/cart");
|
|
3345
|
+
return this.storefrontRequest("POST", "/cart", body);
|
|
3081
3346
|
}
|
|
3082
|
-
return this.adminRequest("POST", "/api/v1/cart");
|
|
3347
|
+
return this.adminRequest("POST", "/api/v1/cart", body);
|
|
3083
3348
|
}
|
|
3084
3349
|
/**
|
|
3085
3350
|
* Get a cart by session token (for guest users)
|
|
@@ -3228,20 +3493,21 @@ var BrainerceClient = class {
|
|
|
3228
3493
|
});
|
|
3229
3494
|
return this.withGuards(this.localCartToCart(this.getLocalCart()), "cart");
|
|
3230
3495
|
}
|
|
3496
|
+
const body = await this.withAnalyticsStitchIds(item);
|
|
3231
3497
|
if (this.isVibeCodedMode()) {
|
|
3232
3498
|
return this.withGuards(
|
|
3233
|
-
this.vibeCodedRequest("POST", `/cart/${encodePathSegment(cartId)}/items`,
|
|
3499
|
+
this.vibeCodedRequest("POST", `/cart/${encodePathSegment(cartId)}/items`, body),
|
|
3234
3500
|
"cart"
|
|
3235
3501
|
);
|
|
3236
3502
|
}
|
|
3237
3503
|
if (this.storeId && !this.apiKey) {
|
|
3238
3504
|
return this.withGuards(
|
|
3239
|
-
this.storefrontRequest("POST", `/cart/${encodePathSegment(cartId)}/items`,
|
|
3505
|
+
this.storefrontRequest("POST", `/cart/${encodePathSegment(cartId)}/items`, body),
|
|
3240
3506
|
"cart"
|
|
3241
3507
|
);
|
|
3242
3508
|
}
|
|
3243
3509
|
return this.withGuards(
|
|
3244
|
-
this.adminRequest("POST", `/api/v1/cart/${encodePathSegment(cartId)}/items`,
|
|
3510
|
+
this.adminRequest("POST", `/api/v1/cart/${encodePathSegment(cartId)}/items`, body),
|
|
3245
3511
|
"cart"
|
|
3246
3512
|
);
|
|
3247
3513
|
}
|
|
@@ -4833,24 +5099,25 @@ var BrainerceClient = class {
|
|
|
4833
5099
|
* ```
|
|
4834
5100
|
*/
|
|
4835
5101
|
async setCheckoutCustomer(checkoutId, data) {
|
|
5102
|
+
const body = await this.withAnalyticsStitchIds(data);
|
|
4836
5103
|
if (this.isVibeCodedMode()) {
|
|
4837
5104
|
return this.vibeCodedRequest(
|
|
4838
5105
|
"PATCH",
|
|
4839
5106
|
`/checkout/${encodePathSegment(checkoutId)}/customer`,
|
|
4840
|
-
|
|
5107
|
+
body
|
|
4841
5108
|
);
|
|
4842
5109
|
}
|
|
4843
5110
|
if (this.storeId && !this.apiKey) {
|
|
4844
5111
|
return this.storefrontRequest(
|
|
4845
5112
|
"PATCH",
|
|
4846
5113
|
`/checkout/${encodePathSegment(checkoutId)}/customer`,
|
|
4847
|
-
|
|
5114
|
+
body
|
|
4848
5115
|
);
|
|
4849
5116
|
}
|
|
4850
5117
|
return this.adminRequest(
|
|
4851
5118
|
"PATCH",
|
|
4852
5119
|
`/api/v1/checkout/${encodePathSegment(checkoutId)}/customer`,
|
|
4853
|
-
|
|
5120
|
+
body
|
|
4854
5121
|
);
|
|
4855
5122
|
}
|
|
4856
5123
|
/**
|
|
@@ -4904,6 +5171,17 @@ var BrainerceClient = class {
|
|
|
4904
5171
|
* should include an optional "Order notes" textarea by default and send its
|
|
4905
5172
|
* value here (or via `setCheckoutCustomer`). The note lands on the order.
|
|
4906
5173
|
*
|
|
5174
|
+
* **Pass `placeId` whenever the address came from `addressAutocomplete()`.**
|
|
5175
|
+
* The server re-resolves it to exact coordinates and matches polygon
|
|
5176
|
+
* ("draw on map") shipping zones against those instead of re-geocoding the
|
|
5177
|
+
* address text — which is materially less accurate and can place the
|
|
5178
|
+
* shopper in a neighbouring city's zone, or in none at all.
|
|
5179
|
+
*
|
|
5180
|
+
* Spreading `getAddressDetails().address` in here is safe: its `lat`, `lng`
|
|
5181
|
+
* and `formattedAddress` are dropped before the request goes out (the
|
|
5182
|
+
* endpoint rejects unknown properties outright, and coordinates are never
|
|
5183
|
+
* taken from the client — the server resolves them from `placeId`).
|
|
5184
|
+
*
|
|
4907
5185
|
* @example
|
|
4908
5186
|
* ```typescript
|
|
4909
5187
|
* const { checkout, rates } = await client.setShippingAddress('checkout_123', {
|
|
@@ -4916,29 +5194,34 @@ var BrainerceClient = class {
|
|
|
4916
5194
|
* postalCode: '10001',
|
|
4917
5195
|
* country: 'US',
|
|
4918
5196
|
* notes: 'Please leave the package at the door', // optional order notes
|
|
5197
|
+
* placeId: suggestion.placeId, // from addressAutocomplete()
|
|
5198
|
+
* placeSessionToken: sessionToken, // the same token used for it
|
|
4919
5199
|
* });
|
|
4920
5200
|
* console.log('Available rates:', rates);
|
|
4921
5201
|
* ```
|
|
4922
5202
|
*/
|
|
4923
5203
|
async setShippingAddress(checkoutId, address) {
|
|
5204
|
+
const body = await this.withAnalyticsStitchIds(
|
|
5205
|
+
this.stripResolvedOnlyAddressFields(address)
|
|
5206
|
+
);
|
|
4924
5207
|
if (this.isVibeCodedMode()) {
|
|
4925
5208
|
return this.vibeCodedRequest(
|
|
4926
5209
|
"PATCH",
|
|
4927
5210
|
`/checkout/${encodePathSegment(checkoutId)}/shipping-address`,
|
|
4928
|
-
|
|
5211
|
+
body
|
|
4929
5212
|
);
|
|
4930
5213
|
}
|
|
4931
5214
|
if (this.storeId && !this.apiKey) {
|
|
4932
5215
|
return this.storefrontRequest(
|
|
4933
5216
|
"PATCH",
|
|
4934
5217
|
`/checkout/${encodePathSegment(checkoutId)}/shipping-address`,
|
|
4935
|
-
|
|
5218
|
+
body
|
|
4936
5219
|
);
|
|
4937
5220
|
}
|
|
4938
5221
|
return this.adminRequest(
|
|
4939
5222
|
"PATCH",
|
|
4940
5223
|
`/api/v1/checkout/${encodePathSegment(checkoutId)}/shipping-address`,
|
|
4941
|
-
|
|
5224
|
+
body
|
|
4942
5225
|
);
|
|
4943
5226
|
}
|
|
4944
5227
|
/**
|
|
@@ -5233,24 +5516,25 @@ var BrainerceClient = class {
|
|
|
5233
5516
|
* ```
|
|
5234
5517
|
*/
|
|
5235
5518
|
async setBillingAddress(checkoutId, address) {
|
|
5519
|
+
const body = this.stripResolvedOnlyAddressFields(address);
|
|
5236
5520
|
if (this.isVibeCodedMode()) {
|
|
5237
5521
|
return this.vibeCodedRequest(
|
|
5238
5522
|
"PATCH",
|
|
5239
5523
|
`/checkout/${encodePathSegment(checkoutId)}/billing-address`,
|
|
5240
|
-
|
|
5524
|
+
body
|
|
5241
5525
|
);
|
|
5242
5526
|
}
|
|
5243
5527
|
if (this.storeId && !this.apiKey) {
|
|
5244
5528
|
return this.storefrontRequest(
|
|
5245
5529
|
"PATCH",
|
|
5246
5530
|
`/checkout/${encodePathSegment(checkoutId)}/billing-address`,
|
|
5247
|
-
|
|
5531
|
+
body
|
|
5248
5532
|
);
|
|
5249
5533
|
}
|
|
5250
5534
|
return this.adminRequest(
|
|
5251
5535
|
"PATCH",
|
|
5252
5536
|
`/api/v1/checkout/${encodePathSegment(checkoutId)}/billing-address`,
|
|
5253
|
-
|
|
5537
|
+
body
|
|
5254
5538
|
);
|
|
5255
5539
|
}
|
|
5256
5540
|
/**
|
|
@@ -6216,7 +6500,7 @@ var BrainerceClient = class {
|
|
|
6216
6500
|
const result = await this.vibeCodedRequest(
|
|
6217
6501
|
"PATCH",
|
|
6218
6502
|
`/checkout/${encodePathSegment(checkoutId)}/shipping-address`,
|
|
6219
|
-
data.shippingAddress
|
|
6503
|
+
this.stripResolvedOnlyAddressFields(data.shippingAddress)
|
|
6220
6504
|
);
|
|
6221
6505
|
checkout = result.checkout;
|
|
6222
6506
|
}
|
|
@@ -6224,7 +6508,7 @@ var BrainerceClient = class {
|
|
|
6224
6508
|
checkout = await this.vibeCodedRequest(
|
|
6225
6509
|
"PATCH",
|
|
6226
6510
|
`/checkout/${encodePathSegment(checkoutId)}/billing-address`,
|
|
6227
|
-
data.billingAddress
|
|
6511
|
+
this.stripResolvedOnlyAddressFields(data.billingAddress)
|
|
6228
6512
|
);
|
|
6229
6513
|
}
|
|
6230
6514
|
if (!checkout) {
|
|
@@ -9065,6 +9349,17 @@ var BrainerceClient = class {
|
|
|
9065
9349
|
);
|
|
9066
9350
|
}
|
|
9067
9351
|
};
|
|
9352
|
+
/**
|
|
9353
|
+
* Fields present on `getAddressDetails().address` that the address endpoints
|
|
9354
|
+
* do NOT accept — stripped by `stripResolvedOnlyAddressFields()` so a
|
|
9355
|
+
* `{ ...address }` spread doesn't 400 the whole checkout.
|
|
9356
|
+
*/
|
|
9357
|
+
_BrainerceClient.RESOLVED_ONLY_ADDRESS_FIELDS = [
|
|
9358
|
+
"lat",
|
|
9359
|
+
"lng",
|
|
9360
|
+
"formattedAddress"
|
|
9361
|
+
];
|
|
9362
|
+
var BrainerceClient = _BrainerceClient;
|
|
9068
9363
|
var BrainerceError = class extends Error {
|
|
9069
9364
|
constructor(message, statusCode, details) {
|
|
9070
9365
|
super(message);
|
|
@@ -9248,6 +9543,11 @@ function formatAmount(amount, currency, locale) {
|
|
|
9248
9543
|
// src/date-availability.ts
|
|
9249
9544
|
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
9250
9545
|
var TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
|
|
9546
|
+
var DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
9547
|
+
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})?$/;
|
|
9548
|
+
var MIN_OFFSET_MINUTES = -12 * 60;
|
|
9549
|
+
var MAX_OFFSET_MINUTES = 14 * 60;
|
|
9550
|
+
var VALID_OFFSET_MINUTE_PARTS = [0, 30, 45];
|
|
9251
9551
|
function validateDateAvailabilityConfig(config, fieldType) {
|
|
9252
9552
|
const errors = [];
|
|
9253
9553
|
if (!config) return errors;
|
|
@@ -9348,6 +9648,101 @@ function resolveStoreLocalParts(instant, timezone) {
|
|
|
9348
9648
|
weekday: WEEKDAY_TO_INDEX[get("weekday")] ?? instant.getUTCDay()
|
|
9349
9649
|
};
|
|
9350
9650
|
}
|
|
9651
|
+
function parseDateFieldValue(raw, fieldType, timezone) {
|
|
9652
|
+
const str = raw instanceof Date ? Number.isNaN(raw.getTime()) ? "" : raw.toISOString() : typeof raw === "string" ? raw.trim() : "";
|
|
9653
|
+
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";
|
|
9654
|
+
if (!str) return { status: "invalid", reason: `"${String(raw)}" is not a valid date \u2014 ${expected}` };
|
|
9655
|
+
const dateOnly = DATE_ONLY_RE.exec(str);
|
|
9656
|
+
const dateTime = dateOnly ? null : DATE_TIME_RE.exec(str);
|
|
9657
|
+
const match = dateOnly ?? dateTime;
|
|
9658
|
+
if (!match) return { status: "invalid", reason: `"${str}" is not a valid date \u2014 ${expected}` };
|
|
9659
|
+
const year = Number(match[1]);
|
|
9660
|
+
const month = Number(match[2]);
|
|
9661
|
+
const day = Number(match[3]);
|
|
9662
|
+
if (!isRealCalendarDate(year, month, day)) {
|
|
9663
|
+
return { status: "invalid", reason: `"${str}" is not a real calendar date` };
|
|
9664
|
+
}
|
|
9665
|
+
const dateYYYYMMDD = `${match[1]}-${match[2]}-${match[3]}`;
|
|
9666
|
+
if (fieldType === "DATE") {
|
|
9667
|
+
return {
|
|
9668
|
+
status: "valid",
|
|
9669
|
+
value: { instant: /* @__PURE__ */ new Date(`${dateYYYYMMDD}T00:00:00.000Z`), normalized: dateYYYYMMDD }
|
|
9670
|
+
};
|
|
9671
|
+
}
|
|
9672
|
+
if (!dateTime) {
|
|
9673
|
+
const instant2 = instantFromStoreLocal(dateYYYYMMDD, 0, 0, 0, 0, timezone);
|
|
9674
|
+
return { status: "valid", value: { instant: instant2, normalized: instant2.toISOString() } };
|
|
9675
|
+
}
|
|
9676
|
+
const hour = Number(dateTime[4]);
|
|
9677
|
+
const minute = Number(dateTime[5]);
|
|
9678
|
+
const second = Number(dateTime[6] ?? "0");
|
|
9679
|
+
const millis = Number((dateTime[7] ?? "").slice(0, 3).padEnd(3, "0") || "0");
|
|
9680
|
+
if (hour > 23 || minute > 59 || second > 59) {
|
|
9681
|
+
return { status: "invalid", reason: `"${str}" has an out-of-range time` };
|
|
9682
|
+
}
|
|
9683
|
+
const offset = dateTime[8];
|
|
9684
|
+
if (!offset) {
|
|
9685
|
+
const instant2 = instantFromStoreLocal(dateYYYYMMDD, hour, minute, second, millis, timezone);
|
|
9686
|
+
return { status: "valid", value: { instant: instant2, normalized: instant2.toISOString() } };
|
|
9687
|
+
}
|
|
9688
|
+
let offsetMinutes = 0;
|
|
9689
|
+
if (offset !== "Z" && offset !== "z") {
|
|
9690
|
+
const offsetHourPart = Number(offset.slice(1, 3));
|
|
9691
|
+
const offsetMinutePart = Number(offset.slice(4, 6));
|
|
9692
|
+
offsetMinutes = (offset[0] === "-" ? -1 : 1) * (offsetHourPart * 60 + offsetMinutePart);
|
|
9693
|
+
if (offsetMinutes < MIN_OFFSET_MINUTES || offsetMinutes > MAX_OFFSET_MINUTES) {
|
|
9694
|
+
return {
|
|
9695
|
+
status: "invalid",
|
|
9696
|
+
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}`
|
|
9697
|
+
};
|
|
9698
|
+
}
|
|
9699
|
+
if (!VALID_OFFSET_MINUTE_PARTS.includes(offsetMinutePart)) {
|
|
9700
|
+
return {
|
|
9701
|
+
status: "invalid",
|
|
9702
|
+
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}`
|
|
9703
|
+
};
|
|
9704
|
+
}
|
|
9705
|
+
}
|
|
9706
|
+
const instant = new Date(
|
|
9707
|
+
Date.UTC(year, month - 1, day, hour, minute, second, millis) - offsetMinutes * 6e4
|
|
9708
|
+
);
|
|
9709
|
+
return { status: "valid", value: { instant, normalized: instant.toISOString() } };
|
|
9710
|
+
}
|
|
9711
|
+
function isRealCalendarDate(year, month, day) {
|
|
9712
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) return false;
|
|
9713
|
+
const probe = new Date(Date.UTC(year, month - 1, day));
|
|
9714
|
+
return probe.getUTCFullYear() === year && probe.getUTCMonth() === month - 1 && probe.getUTCDate() === day;
|
|
9715
|
+
}
|
|
9716
|
+
function instantFromStoreLocal(dateYYYYMMDD, hour, minute, second, millis, timezone) {
|
|
9717
|
+
const [year, month, day] = dateYYYYMMDD.split("-").map(Number);
|
|
9718
|
+
const naive = Date.UTC(year, month - 1, day, hour, minute, second, millis);
|
|
9719
|
+
const firstOffset = timezoneOffsetMs(naive, timezone);
|
|
9720
|
+
let instant = naive - firstOffset;
|
|
9721
|
+
const secondOffset = timezoneOffsetMs(instant, timezone);
|
|
9722
|
+
if (secondOffset !== firstOffset) instant = naive - secondOffset;
|
|
9723
|
+
return new Date(instant);
|
|
9724
|
+
}
|
|
9725
|
+
function timezoneOffsetMs(utcMillis, timezone) {
|
|
9726
|
+
let parts;
|
|
9727
|
+
try {
|
|
9728
|
+
parts = new Intl.DateTimeFormat("en-US", {
|
|
9729
|
+
timeZone: timezone,
|
|
9730
|
+
year: "numeric",
|
|
9731
|
+
month: "2-digit",
|
|
9732
|
+
day: "2-digit",
|
|
9733
|
+
hour: "2-digit",
|
|
9734
|
+
minute: "2-digit",
|
|
9735
|
+
second: "2-digit",
|
|
9736
|
+
hour12: false
|
|
9737
|
+
}).formatToParts(new Date(utcMillis));
|
|
9738
|
+
} catch {
|
|
9739
|
+
return 0;
|
|
9740
|
+
}
|
|
9741
|
+
const get = (type) => Number(parts.find((p) => p.type === type)?.value ?? "0");
|
|
9742
|
+
const hour = get("hour") === 24 ? 0 : get("hour");
|
|
9743
|
+
const asIfUtc = Date.UTC(get("year"), get("month") - 1, get("day"), hour, get("minute"), get("second"));
|
|
9744
|
+
return asIfUtc - Math.floor(utcMillis / 1e3) * 1e3;
|
|
9745
|
+
}
|
|
9351
9746
|
function isCalendarDateAllowed(dateYYYYMMDD, config) {
|
|
9352
9747
|
if (!config) return true;
|
|
9353
9748
|
if (config.minDate && dateYYYYMMDD < config.minDate) return false;
|
|
@@ -9375,6 +9770,12 @@ function computeAvailableSlots(config, dateYYYYMMDD) {
|
|
|
9375
9770
|
}
|
|
9376
9771
|
return slots;
|
|
9377
9772
|
}
|
|
9773
|
+
function getBusinessHoursForDate(config, dateYYYYMMDD) {
|
|
9774
|
+
if (!config?.businessHours?.length) return [];
|
|
9775
|
+
if (!isCalendarDateAllowed(dateYYYYMMDD, config)) return [];
|
|
9776
|
+
const weekday = weekdayOfDateString(dateYYYYMMDD);
|
|
9777
|
+
return config.businessHours.filter((w) => w.weekday === weekday);
|
|
9778
|
+
}
|
|
9378
9779
|
function isDateValueAllowed(instant, config, fieldType, timezone) {
|
|
9379
9780
|
if (!config) return { allowed: true };
|
|
9380
9781
|
if (fieldType === "DATE") {
|
|
@@ -9924,6 +10325,7 @@ function isCouponApplicableToProduct(coupon, productId) {
|
|
|
9924
10325
|
formatProductPrice,
|
|
9925
10326
|
formatVariantPrice,
|
|
9926
10327
|
getBlogSitemapEntries,
|
|
10328
|
+
getBusinessHoursForDate,
|
|
9927
10329
|
getCartItemImage,
|
|
9928
10330
|
getCartItemName,
|
|
9929
10331
|
getCartTotals,
|
|
@@ -9948,6 +10350,7 @@ function isCouponApplicableToProduct(coupon, productId) {
|
|
|
9948
10350
|
isHtmlDescription,
|
|
9949
10351
|
isWebhookEventType,
|
|
9950
10352
|
jsonLdScriptProps,
|
|
10353
|
+
parseDateFieldValue,
|
|
9951
10354
|
parseWebhookEvent,
|
|
9952
10355
|
resolveStoreLocalParts,
|
|
9953
10356
|
safePaymentRedirect,
|