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.mjs
CHANGED
|
@@ -115,7 +115,7 @@ function isDevGuardsEnabled() {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
// src/version.ts
|
|
118
|
-
var SDK_VERSION = "1.
|
|
118
|
+
var SDK_VERSION = "1.53.0";
|
|
119
119
|
|
|
120
120
|
// src/client.ts
|
|
121
121
|
var DEFAULT_BASE_URL = "https://api.brainerce.com";
|
|
@@ -155,7 +155,7 @@ function parseRetryAfterMs(response) {
|
|
|
155
155
|
function sleep(ms) {
|
|
156
156
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
157
157
|
}
|
|
158
|
-
var
|
|
158
|
+
var _BrainerceClient = class _BrainerceClient {
|
|
159
159
|
constructor(options) {
|
|
160
160
|
this.customerToken = null;
|
|
161
161
|
this.customerCartId = null;
|
|
@@ -171,6 +171,11 @@ var BrainerceClient = class {
|
|
|
171
171
|
* so the recovered cart shows up with zero per-store code.
|
|
172
172
|
*/
|
|
173
173
|
this._pendingRecoverCartId = null;
|
|
174
|
+
// GA4 stitch state (see `loadGoogleAnalytics()` in the Analytics section).
|
|
175
|
+
this._ga4MeasurementId = null;
|
|
176
|
+
this._ga4StitchPromise = null;
|
|
177
|
+
/** One warning per client, not per keystroke-driven address submit. */
|
|
178
|
+
this._warnedResolvedOnlyAddressFields = false;
|
|
174
179
|
/** localStorage key for session cart reference (sessionToken + cartId) */
|
|
175
180
|
this.SESSION_CART_KEY = "brainerce_session";
|
|
176
181
|
/**
|
|
@@ -684,10 +689,19 @@ var BrainerceClient = class {
|
|
|
684
689
|
* Set the customer authentication token (obtained from login/register).
|
|
685
690
|
* Required for accessing customer-specific data in storefront mode.
|
|
686
691
|
*
|
|
692
|
+
* This is a plain setter — it authenticates subsequent requests and nothing
|
|
693
|
+
* else. In particular it does NOT attach the shopper's existing guest cart to
|
|
694
|
+
* their account. Pair every sign-in with {@link syncCartOnLogin}, or the cart
|
|
695
|
+
* stays anonymous and every feature keyed on buyer identity degrades quietly:
|
|
696
|
+
* "first order only" discounts keep applying to returning customers,
|
|
697
|
+
* per-customer usage caps stop being enforced at cart time, and abandoned-cart
|
|
698
|
+
* recovery can't identify who to email.
|
|
699
|
+
*
|
|
687
700
|
* @example
|
|
688
701
|
* ```typescript
|
|
689
702
|
* const auth = await client.loginCustomer('user@example.com', 'password');
|
|
690
703
|
* client.setCustomerToken(auth.token);
|
|
704
|
+
* await client.syncCartOnLogin(); // claim the guest cart for this account
|
|
691
705
|
*
|
|
692
706
|
* // Now can access customer data
|
|
693
707
|
* const profile = await client.getMyProfile();
|
|
@@ -1081,6 +1095,159 @@ var BrainerceClient = class {
|
|
|
1081
1095
|
} catch {
|
|
1082
1096
|
}
|
|
1083
1097
|
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Load GA4's `gtag.js` and start resolving the `client_id`/`session_id`
|
|
1100
|
+
* "stitch" ids Google needs to join a later server-side purchase conversion
|
|
1101
|
+
* to this browser's GA4 session (without them, a server-sent purchase event
|
|
1102
|
+
* either gets rejected or creates a phantom user in GA4). Call this once,
|
|
1103
|
+
* as early as possible (app entry / root layout).
|
|
1104
|
+
*
|
|
1105
|
+
* `createCart()`, `addToCart()`, `setCheckoutCustomer()`, and
|
|
1106
|
+
* `setShippingAddress()` all auto-attach the resolved ids once available —
|
|
1107
|
+
* an explicit `analyticsClientId`/`analyticsSessionId` you pass to any of
|
|
1108
|
+
* those always wins over the auto-captured value. You only need to call
|
|
1109
|
+
* this once; every subsequent cart/checkout call benefits automatically.
|
|
1110
|
+
*
|
|
1111
|
+
* Ids are resolved via `gtag('get', measurementId, 'client_id' | 'session_id', cb)`
|
|
1112
|
+
* — Google's documented method — never by parsing the `_ga` cookie, which
|
|
1113
|
+
* is undocumented and breaks silently across cookie-format changes and
|
|
1114
|
+
* Consent Mode v2 states. If the shopper has denied analytics consent,
|
|
1115
|
+
* gtag reports no client_id and the ids are simply omitted — never
|
|
1116
|
+
* synthesized.
|
|
1117
|
+
*
|
|
1118
|
+
* No-op outside the browser (SSR-safe) and never throws — a blocked or
|
|
1119
|
+
* slow gtag just means server-side conversions won't stitch; it never
|
|
1120
|
+
* breaks the storefront or delays checkout by more than `options.timeoutMs`.
|
|
1121
|
+
*
|
|
1122
|
+
* @example
|
|
1123
|
+
* ```typescript
|
|
1124
|
+
* // Call once, e.g. in your root layout / app entry point
|
|
1125
|
+
* client.loadGoogleAnalytics('G-XXXXXXX');
|
|
1126
|
+
*
|
|
1127
|
+
* // Every cart/checkout call from here on auto-forwards the stitch ids —
|
|
1128
|
+
* // no other code changes needed.
|
|
1129
|
+
* const cart = await client.createCart();
|
|
1130
|
+
* await client.addToCart(cart.id, { productId: 'prod_abc', quantity: 1 });
|
|
1131
|
+
* ```
|
|
1132
|
+
*/
|
|
1133
|
+
loadGoogleAnalytics(measurementId, options) {
|
|
1134
|
+
if (typeof window === "undefined" || !measurementId) return;
|
|
1135
|
+
if (this._ga4MeasurementId === measurementId && this._ga4StitchPromise) return;
|
|
1136
|
+
this._ga4MeasurementId = measurementId;
|
|
1137
|
+
try {
|
|
1138
|
+
if (!window.gtag) {
|
|
1139
|
+
window.dataLayer = window.dataLayer || [];
|
|
1140
|
+
const gtag = (...args) => {
|
|
1141
|
+
window.dataLayer.push(args);
|
|
1142
|
+
};
|
|
1143
|
+
window.gtag = gtag;
|
|
1144
|
+
gtag("js", /* @__PURE__ */ new Date());
|
|
1145
|
+
const script = document.createElement("script");
|
|
1146
|
+
script.async = true;
|
|
1147
|
+
script.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`;
|
|
1148
|
+
document.head.appendChild(script);
|
|
1149
|
+
}
|
|
1150
|
+
window.gtag("config", measurementId);
|
|
1151
|
+
} catch {
|
|
1152
|
+
}
|
|
1153
|
+
this._ga4StitchPromise = this.resolveGa4StitchIds(measurementId, options?.timeoutMs ?? 1500);
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Resolve GA4's `client_id`/`session_id` via `gtag('get', ...)`, bounded by
|
|
1157
|
+
* `timeoutMs` so a slow/blocked gtag never hangs a cart/checkout call.
|
|
1158
|
+
* Resolves to `{}` (never rejects) on timeout, missing gtag, or denied
|
|
1159
|
+
* consent.
|
|
1160
|
+
*/
|
|
1161
|
+
resolveGa4StitchIds(measurementId, timeoutMs) {
|
|
1162
|
+
return new Promise((resolve) => {
|
|
1163
|
+
if (typeof window === "undefined" || !window.gtag) {
|
|
1164
|
+
resolve({});
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
const result = {};
|
|
1168
|
+
let settled = false;
|
|
1169
|
+
const finish = () => {
|
|
1170
|
+
if (settled) return;
|
|
1171
|
+
settled = true;
|
|
1172
|
+
resolve(result);
|
|
1173
|
+
};
|
|
1174
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
1175
|
+
let pending = 2;
|
|
1176
|
+
const done = () => {
|
|
1177
|
+
pending -= 1;
|
|
1178
|
+
if (pending === 0) {
|
|
1179
|
+
clearTimeout(timer);
|
|
1180
|
+
finish();
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
try {
|
|
1184
|
+
window.gtag("get", measurementId, "client_id", (id) => {
|
|
1185
|
+
if (id) result.analyticsClientId = id;
|
|
1186
|
+
done();
|
|
1187
|
+
});
|
|
1188
|
+
window.gtag("get", measurementId, "session_id", (id) => {
|
|
1189
|
+
if (id) result.analyticsSessionId = String(id);
|
|
1190
|
+
done();
|
|
1191
|
+
});
|
|
1192
|
+
} catch {
|
|
1193
|
+
clearTimeout(timer);
|
|
1194
|
+
finish();
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Merge the resolved GA4 stitch ids onto a request body — only for fields
|
|
1200
|
+
* the caller didn't already set explicitly (explicit values always win).
|
|
1201
|
+
* No-op (returns `dto` unchanged) if `loadGoogleAnalytics()` was never
|
|
1202
|
+
* called, or if it hasn't resolved any ids by the time this is awaited.
|
|
1203
|
+
*/
|
|
1204
|
+
async withAnalyticsStitchIds(dto) {
|
|
1205
|
+
if (!this._ga4StitchPromise) return dto;
|
|
1206
|
+
try {
|
|
1207
|
+
const ids = await this._ga4StitchPromise;
|
|
1208
|
+
if (!ids.analyticsClientId && !ids.analyticsSessionId) return dto;
|
|
1209
|
+
return {
|
|
1210
|
+
...dto ?? {},
|
|
1211
|
+
analyticsClientId: dto?.analyticsClientId ?? ids.analyticsClientId,
|
|
1212
|
+
analyticsSessionId: dto?.analyticsSessionId ?? ids.analyticsSessionId
|
|
1213
|
+
};
|
|
1214
|
+
} catch {
|
|
1215
|
+
return dto;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Drop the fields `getAddressDetails()` returns that no address endpoint
|
|
1220
|
+
* accepts, so spreading its `address` straight into `setShippingAddress()`
|
|
1221
|
+
* / `setBillingAddress()` works instead of failing the whole request.
|
|
1222
|
+
*
|
|
1223
|
+
* The address endpoints validate against a strict allow-list: ONE unknown
|
|
1224
|
+
* property rejects the call with `400 "property lat should not exist"`, and
|
|
1225
|
+
* the shopper cannot check out at all. `lat`/`lng`/`formattedAddress` are
|
|
1226
|
+
* the only realistic way to hit that — they come out of this SDK's own
|
|
1227
|
+
* resolved-address shape, so this SDK cleans up after itself rather than
|
|
1228
|
+
* making every storefront remember to. Nothing else is stripped: a genuine
|
|
1229
|
+
* typo still reaches the server and still fails loudly.
|
|
1230
|
+
*
|
|
1231
|
+
* Coordinates are dropped rather than forwarded because zone matching picks
|
|
1232
|
+
* which shipping rate is offered and charged — the server resolves them
|
|
1233
|
+
* itself from `placeId`, and never takes them from the caller.
|
|
1234
|
+
*/
|
|
1235
|
+
stripResolvedOnlyAddressFields(address) {
|
|
1236
|
+
if (!address || typeof address !== "object") return address;
|
|
1237
|
+
const present = _BrainerceClient.RESOLVED_ONLY_ADDRESS_FIELDS.filter(
|
|
1238
|
+
(field) => field in address
|
|
1239
|
+
);
|
|
1240
|
+
if (present.length === 0) return address;
|
|
1241
|
+
const cleaned = { ...address };
|
|
1242
|
+
for (const field of present) delete cleaned[field];
|
|
1243
|
+
if (!this._warnedResolvedOnlyAddressFields) {
|
|
1244
|
+
this._warnedResolvedOnlyAddressFields = true;
|
|
1245
|
+
console.warn(
|
|
1246
|
+
`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.`
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
return cleaned;
|
|
1250
|
+
}
|
|
1084
1251
|
// -------------------- Products --------------------
|
|
1085
1252
|
/**
|
|
1086
1253
|
* Get a list of products with pagination and filtering
|
|
@@ -1801,13 +1968,23 @@ var BrainerceClient = class {
|
|
|
1801
1968
|
}
|
|
1802
1969
|
/**
|
|
1803
1970
|
* Create a shipping label for an order via the installed App Store shipping app.
|
|
1804
|
-
* Pass the rate ID returned from
|
|
1805
|
-
* to the merchant's carrier account
|
|
1971
|
+
* Pass the rate ID returned from rate shopping — treat it as opaque and never
|
|
1972
|
+
* parse it. Billing goes directly to the merchant's carrier account; Brainerce
|
|
1973
|
+
* is not a billing intermediary.
|
|
1974
|
+
*
|
|
1975
|
+
* `labelFormat` defaults to `PDF`. Use `ZPL` or `EPL` for warehouse thermal
|
|
1976
|
+
* printers. If the carrier cannot produce the requested format it returns its
|
|
1977
|
+
* closest match rather than failing the purchase — check the response.
|
|
1978
|
+
*
|
|
1979
|
+
* Once the label exists, tracking updates arrive automatically: the carrier's
|
|
1980
|
+
* webhooks move the shipment through in-transit → delivered and complete the
|
|
1981
|
+
* order. No polling required.
|
|
1806
1982
|
*
|
|
1807
1983
|
* @example
|
|
1808
1984
|
* ```typescript
|
|
1809
1985
|
* const label = await client.createShippingLabel('order_abc', {
|
|
1810
1986
|
* rateId: 'rate_8f123456789abcdef',
|
|
1987
|
+
* labelFormat: 'ZPL',
|
|
1811
1988
|
* });
|
|
1812
1989
|
* console.log('Label URL:', label.labelUrl);
|
|
1813
1990
|
* console.log('Tracking:', label.trackingNumber);
|
|
@@ -1820,6 +1997,37 @@ var BrainerceClient = class {
|
|
|
1820
1997
|
data
|
|
1821
1998
|
);
|
|
1822
1999
|
}
|
|
2000
|
+
/**
|
|
2001
|
+
* Live carrier rates for an order, from the merchant's installed shipping app.
|
|
2002
|
+
*
|
|
2003
|
+
* Call this immediately before {@link createShippingLabel} and pass the chosen
|
|
2004
|
+
* `id` straight through — the id is opaque and must not be parsed. It is also
|
|
2005
|
+
* short-lived: this call is what creates the shipment at the carrier, so a
|
|
2006
|
+
* rate from an old call may no longer be purchasable.
|
|
2007
|
+
*
|
|
2008
|
+
* Returns `[]` when the store has no shipping app installed, or when the
|
|
2009
|
+
* order has no usable shipping address.
|
|
2010
|
+
*
|
|
2011
|
+
* @example
|
|
2012
|
+
* ```typescript
|
|
2013
|
+
* const rates = await client.getOrderShippingRates('order_abc');
|
|
2014
|
+
* const cheapest = rates[0];
|
|
2015
|
+
* const label = await client.createShippingLabel('order_abc', { rateId: cheapest.id });
|
|
2016
|
+
* ```
|
|
2017
|
+
*/
|
|
2018
|
+
async getOrderShippingRates(orderId) {
|
|
2019
|
+
return this.request("GET", `/api/v1/orders/${encodePathSegment(orderId)}/shipments/app-rates`);
|
|
2020
|
+
}
|
|
2021
|
+
/**
|
|
2022
|
+
* Shipments recorded for an order, each with its tracking history.
|
|
2023
|
+
*
|
|
2024
|
+
* History arrives on its own: the carrier's webhooks flow through the
|
|
2025
|
+
* installed shipping app and append events as the parcel moves. Poll this for
|
|
2026
|
+
* display if you need it; do not poll expecting to *drive* anything.
|
|
2027
|
+
*/
|
|
2028
|
+
async getOrderShipments(orderId) {
|
|
2029
|
+
return this.request("GET", `/api/v1/orders/${encodePathSegment(orderId)}/shipments`);
|
|
2030
|
+
}
|
|
1823
2031
|
/**
|
|
1824
2032
|
* Cancel an order
|
|
1825
2033
|
* Works for Shopify and WooCommerce orders that haven't been fulfilled
|
|
@@ -1834,16 +2042,31 @@ var BrainerceClient = class {
|
|
|
1834
2042
|
return this.request("POST", `/api/v1/orders/${encodePathSegment(orderId)}/cancel`);
|
|
1835
2043
|
}
|
|
1836
2044
|
/**
|
|
1837
|
-
* Fulfill an order (mark as shipped)
|
|
1838
|
-
*
|
|
2045
|
+
* Fulfill an order (mark as shipped), or correct the tracking of an order
|
|
2046
|
+
* that has already shipped.
|
|
2047
|
+
*
|
|
2048
|
+
* Pass `trackingUrl` alongside the number — the shipped email only renders
|
|
2049
|
+
* its "Track Your Order" button when a URL is present.
|
|
2050
|
+
*
|
|
2051
|
+
* Calling this again on an order that is already `SHIPPED`/`FULFILLED` with
|
|
2052
|
+
* tracking fields edits only those fields: the status does not move, the
|
|
2053
|
+
* ship date is not rewritten, and no fulfilment event fires. That is the way
|
|
2054
|
+
* to fix a mistyped tracking number.
|
|
1839
2055
|
*
|
|
1840
2056
|
* @example
|
|
1841
2057
|
* ```typescript
|
|
1842
|
-
*
|
|
2058
|
+
* // First fulfilment — emails the shopper by default.
|
|
2059
|
+
* await client.fulfillOrder('order_123', {
|
|
1843
2060
|
* trackingNumber: '1Z999AA10123456784',
|
|
1844
2061
|
* trackingCompany: 'UPS',
|
|
2062
|
+
* trackingUrl: 'https://www.ups.com/track?tracknum=1Z999AA10123456784',
|
|
1845
2063
|
* notifyCustomer: true,
|
|
1846
2064
|
* });
|
|
2065
|
+
*
|
|
2066
|
+
* // Correction — silent unless you opt back in.
|
|
2067
|
+
* await client.fulfillOrder('order_123', {
|
|
2068
|
+
* trackingNumber: '1Z999AA10123456785',
|
|
2069
|
+
* });
|
|
1847
2070
|
* ```
|
|
1848
2071
|
*/
|
|
1849
2072
|
async fulfillOrder(orderId, data) {
|
|
@@ -2584,11 +2807,25 @@ var BrainerceClient = class {
|
|
|
2584
2807
|
*
|
|
2585
2808
|
* @param provider - OAuth provider ('GOOGLE', 'FACEBOOK', 'GITHUB')
|
|
2586
2809
|
* @param options - Optional configuration
|
|
2587
|
-
* @param options.redirectUrl -
|
|
2588
|
-
*
|
|
2589
|
-
*
|
|
2590
|
-
*
|
|
2591
|
-
*
|
|
2810
|
+
* @param options.redirectUrl - Where to send the browser once OAuth finishes —
|
|
2811
|
+
* on success *and* on failure. Validated server-side against the sales
|
|
2812
|
+
* channel's trusted origins, so what is accepted depends on the mode:
|
|
2813
|
+
* - vibe-coded (`salesChannelId: 'vc_*'`): an absolute URL on the channel's
|
|
2814
|
+
* registered `domain` or one of its `allowedOrigins`; in TEST mode, any
|
|
2815
|
+
* `localhost`/`127.0.0.1` port. A relative path (`/auth/callback`) also
|
|
2816
|
+
* works — it is resolved against the channel's `domain` on the way back,
|
|
2817
|
+
* so the channel must have one registered.
|
|
2818
|
+
* - storefront (`storeId`): **social login cannot round-trip in this mode.**
|
|
2819
|
+
* No channel is bound to the request, so an absolute URL has no
|
|
2820
|
+
* trusted-origin list to match (400 at this call) and a relative path has
|
|
2821
|
+
* no origin to resolve against on the way back. Use a `salesChannelId`
|
|
2822
|
+
* connection for OAuth.
|
|
2823
|
+
* Anything invalid fails fast here, before the shopper ever reaches the
|
|
2824
|
+
* provider.
|
|
2825
|
+
*
|
|
2826
|
+
* @example
|
|
2827
|
+
* ```typescript
|
|
2828
|
+
* // Vibe-coded mode — absolute URL on the registered storefront domain
|
|
2592
2829
|
* const { authorizationUrl } = await client.getOAuthAuthorizeUrl('GOOGLE', {
|
|
2593
2830
|
* redirectUrl: window.location.origin + '/auth/callback'
|
|
2594
2831
|
* });
|
|
@@ -2603,9 +2840,25 @@ var BrainerceClient = class {
|
|
|
2603
2840
|
* if (params.get('oauth_success') === 'true' && params.get('auth_code')) {
|
|
2604
2841
|
* const result = await client.exchangeOAuthCode(params.get('auth_code')!);
|
|
2605
2842
|
* client.setCustomerToken(result.token);
|
|
2843
|
+
* // REQUIRED: setCustomerToken only stores the JWT — it does NOT attach the
|
|
2844
|
+
* // guest cart to the account. Without this call the cart stays anonymous,
|
|
2845
|
+
* // and anything keyed on the buyer's identity misbehaves: "first order
|
|
2846
|
+
* // only" discounts re-apply to returning customers, per-customer usage
|
|
2847
|
+
* // caps go unenforced, and abandoned-cart recovery can't reach them.
|
|
2848
|
+
* await client.syncCartOnLogin();
|
|
2606
2849
|
* // result.customer, result.isNewCustomer, result.redirectUrl, ...
|
|
2607
2850
|
* } else if (params.get('oauth_error')) {
|
|
2608
|
-
* //
|
|
2851
|
+
* // Failures land on this same page, on `redirectUrl` — never on the API
|
|
2852
|
+
* // host. `oauth_error` is a stable snake_case code (see OAuthErrorCode);
|
|
2853
|
+
* // `error_description` is English developer detail, not shopper copy.
|
|
2854
|
+
* const code = params.get('oauth_error') as OAuthErrorCode;
|
|
2855
|
+
* showMessage(
|
|
2856
|
+
* code === 'link_blocked_unverified_password_account'
|
|
2857
|
+
* ? t('auth.verifyEmailFirst') // send them to email verification
|
|
2858
|
+
* : code === 'state_expired'
|
|
2859
|
+
* ? t('auth.sessionExpiredRetry')
|
|
2860
|
+
* : t('auth.signInFailed')
|
|
2861
|
+
* );
|
|
2609
2862
|
* }
|
|
2610
2863
|
* ```
|
|
2611
2864
|
*/
|
|
@@ -2645,6 +2898,11 @@ var BrainerceClient = class {
|
|
|
2645
2898
|
*
|
|
2646
2899
|
* @param authCode - The single-use code from the `?auth_code=` URL param.
|
|
2647
2900
|
*
|
|
2901
|
+
* Always follow a successful exchange with `syncCartOnLogin()`. Storing the
|
|
2902
|
+
* token does not claim the guest cart, and an unclaimed cart has no buyer
|
|
2903
|
+
* identity — which silently breaks first-order discounts, per-customer usage
|
|
2904
|
+
* caps, and abandoned-cart recovery for everyone who signs in with OAuth.
|
|
2905
|
+
*
|
|
2648
2906
|
* @example
|
|
2649
2907
|
* ```typescript
|
|
2650
2908
|
* const params = new URLSearchParams(window.location.search);
|
|
@@ -2653,6 +2911,7 @@ var BrainerceClient = class {
|
|
|
2653
2911
|
* const { token, customer, isNewCustomer, redirectUrl } =
|
|
2654
2912
|
* await client.exchangeOAuthCode(code);
|
|
2655
2913
|
* client.setCustomerToken(token);
|
|
2914
|
+
* await client.syncCartOnLogin(); // attach the guest cart to the account
|
|
2656
2915
|
* }
|
|
2657
2916
|
* ```
|
|
2658
2917
|
*/
|
|
@@ -2981,6 +3240,9 @@ var BrainerceClient = class {
|
|
|
2981
3240
|
* Create a new cart for a guest user
|
|
2982
3241
|
* Returns a cart with a sessionToken that identifies this cart
|
|
2983
3242
|
*
|
|
3243
|
+
* If `loadGoogleAnalytics()` was called and has resolved a GA4 client/session
|
|
3244
|
+
* id, it's auto-attached unless `options` already specifies one.
|
|
3245
|
+
*
|
|
2984
3246
|
* @example
|
|
2985
3247
|
* ```typescript
|
|
2986
3248
|
* const cart = await client.createCart();
|
|
@@ -2988,14 +3250,15 @@ var BrainerceClient = class {
|
|
|
2988
3250
|
* // Store sessionToken in localStorage or cookie
|
|
2989
3251
|
* ```
|
|
2990
3252
|
*/
|
|
2991
|
-
async createCart() {
|
|
3253
|
+
async createCart(options) {
|
|
3254
|
+
const body = await this.withAnalyticsStitchIds(options);
|
|
2992
3255
|
if (this.isVibeCodedMode()) {
|
|
2993
|
-
return this.vibeCodedRequest("POST", "/cart");
|
|
3256
|
+
return this.vibeCodedRequest("POST", "/cart", body);
|
|
2994
3257
|
}
|
|
2995
3258
|
if (this.storeId && !this.apiKey) {
|
|
2996
|
-
return this.storefrontRequest("POST", "/cart");
|
|
3259
|
+
return this.storefrontRequest("POST", "/cart", body);
|
|
2997
3260
|
}
|
|
2998
|
-
return this.adminRequest("POST", "/api/v1/cart");
|
|
3261
|
+
return this.adminRequest("POST", "/api/v1/cart", body);
|
|
2999
3262
|
}
|
|
3000
3263
|
/**
|
|
3001
3264
|
* Get a cart by session token (for guest users)
|
|
@@ -3144,20 +3407,21 @@ var BrainerceClient = class {
|
|
|
3144
3407
|
});
|
|
3145
3408
|
return this.withGuards(this.localCartToCart(this.getLocalCart()), "cart");
|
|
3146
3409
|
}
|
|
3410
|
+
const body = await this.withAnalyticsStitchIds(item);
|
|
3147
3411
|
if (this.isVibeCodedMode()) {
|
|
3148
3412
|
return this.withGuards(
|
|
3149
|
-
this.vibeCodedRequest("POST", `/cart/${encodePathSegment(cartId)}/items`,
|
|
3413
|
+
this.vibeCodedRequest("POST", `/cart/${encodePathSegment(cartId)}/items`, body),
|
|
3150
3414
|
"cart"
|
|
3151
3415
|
);
|
|
3152
3416
|
}
|
|
3153
3417
|
if (this.storeId && !this.apiKey) {
|
|
3154
3418
|
return this.withGuards(
|
|
3155
|
-
this.storefrontRequest("POST", `/cart/${encodePathSegment(cartId)}/items`,
|
|
3419
|
+
this.storefrontRequest("POST", `/cart/${encodePathSegment(cartId)}/items`, body),
|
|
3156
3420
|
"cart"
|
|
3157
3421
|
);
|
|
3158
3422
|
}
|
|
3159
3423
|
return this.withGuards(
|
|
3160
|
-
this.adminRequest("POST", `/api/v1/cart/${encodePathSegment(cartId)}/items`,
|
|
3424
|
+
this.adminRequest("POST", `/api/v1/cart/${encodePathSegment(cartId)}/items`, body),
|
|
3161
3425
|
"cart"
|
|
3162
3426
|
);
|
|
3163
3427
|
}
|
|
@@ -4749,24 +5013,25 @@ var BrainerceClient = class {
|
|
|
4749
5013
|
* ```
|
|
4750
5014
|
*/
|
|
4751
5015
|
async setCheckoutCustomer(checkoutId, data) {
|
|
5016
|
+
const body = await this.withAnalyticsStitchIds(data);
|
|
4752
5017
|
if (this.isVibeCodedMode()) {
|
|
4753
5018
|
return this.vibeCodedRequest(
|
|
4754
5019
|
"PATCH",
|
|
4755
5020
|
`/checkout/${encodePathSegment(checkoutId)}/customer`,
|
|
4756
|
-
|
|
5021
|
+
body
|
|
4757
5022
|
);
|
|
4758
5023
|
}
|
|
4759
5024
|
if (this.storeId && !this.apiKey) {
|
|
4760
5025
|
return this.storefrontRequest(
|
|
4761
5026
|
"PATCH",
|
|
4762
5027
|
`/checkout/${encodePathSegment(checkoutId)}/customer`,
|
|
4763
|
-
|
|
5028
|
+
body
|
|
4764
5029
|
);
|
|
4765
5030
|
}
|
|
4766
5031
|
return this.adminRequest(
|
|
4767
5032
|
"PATCH",
|
|
4768
5033
|
`/api/v1/checkout/${encodePathSegment(checkoutId)}/customer`,
|
|
4769
|
-
|
|
5034
|
+
body
|
|
4770
5035
|
);
|
|
4771
5036
|
}
|
|
4772
5037
|
/**
|
|
@@ -4820,6 +5085,17 @@ var BrainerceClient = class {
|
|
|
4820
5085
|
* should include an optional "Order notes" textarea by default and send its
|
|
4821
5086
|
* value here (or via `setCheckoutCustomer`). The note lands on the order.
|
|
4822
5087
|
*
|
|
5088
|
+
* **Pass `placeId` whenever the address came from `addressAutocomplete()`.**
|
|
5089
|
+
* The server re-resolves it to exact coordinates and matches polygon
|
|
5090
|
+
* ("draw on map") shipping zones against those instead of re-geocoding the
|
|
5091
|
+
* address text — which is materially less accurate and can place the
|
|
5092
|
+
* shopper in a neighbouring city's zone, or in none at all.
|
|
5093
|
+
*
|
|
5094
|
+
* Spreading `getAddressDetails().address` in here is safe: its `lat`, `lng`
|
|
5095
|
+
* and `formattedAddress` are dropped before the request goes out (the
|
|
5096
|
+
* endpoint rejects unknown properties outright, and coordinates are never
|
|
5097
|
+
* taken from the client — the server resolves them from `placeId`).
|
|
5098
|
+
*
|
|
4823
5099
|
* @example
|
|
4824
5100
|
* ```typescript
|
|
4825
5101
|
* const { checkout, rates } = await client.setShippingAddress('checkout_123', {
|
|
@@ -4832,29 +5108,34 @@ var BrainerceClient = class {
|
|
|
4832
5108
|
* postalCode: '10001',
|
|
4833
5109
|
* country: 'US',
|
|
4834
5110
|
* notes: 'Please leave the package at the door', // optional order notes
|
|
5111
|
+
* placeId: suggestion.placeId, // from addressAutocomplete()
|
|
5112
|
+
* placeSessionToken: sessionToken, // the same token used for it
|
|
4835
5113
|
* });
|
|
4836
5114
|
* console.log('Available rates:', rates);
|
|
4837
5115
|
* ```
|
|
4838
5116
|
*/
|
|
4839
5117
|
async setShippingAddress(checkoutId, address) {
|
|
5118
|
+
const body = await this.withAnalyticsStitchIds(
|
|
5119
|
+
this.stripResolvedOnlyAddressFields(address)
|
|
5120
|
+
);
|
|
4840
5121
|
if (this.isVibeCodedMode()) {
|
|
4841
5122
|
return this.vibeCodedRequest(
|
|
4842
5123
|
"PATCH",
|
|
4843
5124
|
`/checkout/${encodePathSegment(checkoutId)}/shipping-address`,
|
|
4844
|
-
|
|
5125
|
+
body
|
|
4845
5126
|
);
|
|
4846
5127
|
}
|
|
4847
5128
|
if (this.storeId && !this.apiKey) {
|
|
4848
5129
|
return this.storefrontRequest(
|
|
4849
5130
|
"PATCH",
|
|
4850
5131
|
`/checkout/${encodePathSegment(checkoutId)}/shipping-address`,
|
|
4851
|
-
|
|
5132
|
+
body
|
|
4852
5133
|
);
|
|
4853
5134
|
}
|
|
4854
5135
|
return this.adminRequest(
|
|
4855
5136
|
"PATCH",
|
|
4856
5137
|
`/api/v1/checkout/${encodePathSegment(checkoutId)}/shipping-address`,
|
|
4857
|
-
|
|
5138
|
+
body
|
|
4858
5139
|
);
|
|
4859
5140
|
}
|
|
4860
5141
|
/**
|
|
@@ -5149,24 +5430,25 @@ var BrainerceClient = class {
|
|
|
5149
5430
|
* ```
|
|
5150
5431
|
*/
|
|
5151
5432
|
async setBillingAddress(checkoutId, address) {
|
|
5433
|
+
const body = this.stripResolvedOnlyAddressFields(address);
|
|
5152
5434
|
if (this.isVibeCodedMode()) {
|
|
5153
5435
|
return this.vibeCodedRequest(
|
|
5154
5436
|
"PATCH",
|
|
5155
5437
|
`/checkout/${encodePathSegment(checkoutId)}/billing-address`,
|
|
5156
|
-
|
|
5438
|
+
body
|
|
5157
5439
|
);
|
|
5158
5440
|
}
|
|
5159
5441
|
if (this.storeId && !this.apiKey) {
|
|
5160
5442
|
return this.storefrontRequest(
|
|
5161
5443
|
"PATCH",
|
|
5162
5444
|
`/checkout/${encodePathSegment(checkoutId)}/billing-address`,
|
|
5163
|
-
|
|
5445
|
+
body
|
|
5164
5446
|
);
|
|
5165
5447
|
}
|
|
5166
5448
|
return this.adminRequest(
|
|
5167
5449
|
"PATCH",
|
|
5168
5450
|
`/api/v1/checkout/${encodePathSegment(checkoutId)}/billing-address`,
|
|
5169
|
-
|
|
5451
|
+
body
|
|
5170
5452
|
);
|
|
5171
5453
|
}
|
|
5172
5454
|
/**
|
|
@@ -6132,7 +6414,7 @@ var BrainerceClient = class {
|
|
|
6132
6414
|
const result = await this.vibeCodedRequest(
|
|
6133
6415
|
"PATCH",
|
|
6134
6416
|
`/checkout/${encodePathSegment(checkoutId)}/shipping-address`,
|
|
6135
|
-
data.shippingAddress
|
|
6417
|
+
this.stripResolvedOnlyAddressFields(data.shippingAddress)
|
|
6136
6418
|
);
|
|
6137
6419
|
checkout = result.checkout;
|
|
6138
6420
|
}
|
|
@@ -6140,7 +6422,7 @@ var BrainerceClient = class {
|
|
|
6140
6422
|
checkout = await this.vibeCodedRequest(
|
|
6141
6423
|
"PATCH",
|
|
6142
6424
|
`/checkout/${encodePathSegment(checkoutId)}/billing-address`,
|
|
6143
|
-
data.billingAddress
|
|
6425
|
+
this.stripResolvedOnlyAddressFields(data.billingAddress)
|
|
6144
6426
|
);
|
|
6145
6427
|
}
|
|
6146
6428
|
if (!checkout) {
|
|
@@ -8981,6 +9263,17 @@ var BrainerceClient = class {
|
|
|
8981
9263
|
);
|
|
8982
9264
|
}
|
|
8983
9265
|
};
|
|
9266
|
+
/**
|
|
9267
|
+
* Fields present on `getAddressDetails().address` that the address endpoints
|
|
9268
|
+
* do NOT accept — stripped by `stripResolvedOnlyAddressFields()` so a
|
|
9269
|
+
* `{ ...address }` spread doesn't 400 the whole checkout.
|
|
9270
|
+
*/
|
|
9271
|
+
_BrainerceClient.RESOLVED_ONLY_ADDRESS_FIELDS = [
|
|
9272
|
+
"lat",
|
|
9273
|
+
"lng",
|
|
9274
|
+
"formattedAddress"
|
|
9275
|
+
];
|
|
9276
|
+
var BrainerceClient = _BrainerceClient;
|
|
8984
9277
|
var BrainerceError = class extends Error {
|
|
8985
9278
|
constructor(message, statusCode, details) {
|
|
8986
9279
|
super(message);
|
|
@@ -9164,6 +9457,11 @@ function formatAmount(amount, currency, locale) {
|
|
|
9164
9457
|
// src/date-availability.ts
|
|
9165
9458
|
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
9166
9459
|
var TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
|
|
9460
|
+
var DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
9461
|
+
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})?$/;
|
|
9462
|
+
var MIN_OFFSET_MINUTES = -12 * 60;
|
|
9463
|
+
var MAX_OFFSET_MINUTES = 14 * 60;
|
|
9464
|
+
var VALID_OFFSET_MINUTE_PARTS = [0, 30, 45];
|
|
9167
9465
|
function validateDateAvailabilityConfig(config, fieldType) {
|
|
9168
9466
|
const errors = [];
|
|
9169
9467
|
if (!config) return errors;
|
|
@@ -9264,6 +9562,101 @@ function resolveStoreLocalParts(instant, timezone) {
|
|
|
9264
9562
|
weekday: WEEKDAY_TO_INDEX[get("weekday")] ?? instant.getUTCDay()
|
|
9265
9563
|
};
|
|
9266
9564
|
}
|
|
9565
|
+
function parseDateFieldValue(raw, fieldType, timezone) {
|
|
9566
|
+
const str = raw instanceof Date ? Number.isNaN(raw.getTime()) ? "" : raw.toISOString() : typeof raw === "string" ? raw.trim() : "";
|
|
9567
|
+
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";
|
|
9568
|
+
if (!str) return { status: "invalid", reason: `"${String(raw)}" is not a valid date \u2014 ${expected}` };
|
|
9569
|
+
const dateOnly = DATE_ONLY_RE.exec(str);
|
|
9570
|
+
const dateTime = dateOnly ? null : DATE_TIME_RE.exec(str);
|
|
9571
|
+
const match = dateOnly ?? dateTime;
|
|
9572
|
+
if (!match) return { status: "invalid", reason: `"${str}" is not a valid date \u2014 ${expected}` };
|
|
9573
|
+
const year = Number(match[1]);
|
|
9574
|
+
const month = Number(match[2]);
|
|
9575
|
+
const day = Number(match[3]);
|
|
9576
|
+
if (!isRealCalendarDate(year, month, day)) {
|
|
9577
|
+
return { status: "invalid", reason: `"${str}" is not a real calendar date` };
|
|
9578
|
+
}
|
|
9579
|
+
const dateYYYYMMDD = `${match[1]}-${match[2]}-${match[3]}`;
|
|
9580
|
+
if (fieldType === "DATE") {
|
|
9581
|
+
return {
|
|
9582
|
+
status: "valid",
|
|
9583
|
+
value: { instant: /* @__PURE__ */ new Date(`${dateYYYYMMDD}T00:00:00.000Z`), normalized: dateYYYYMMDD }
|
|
9584
|
+
};
|
|
9585
|
+
}
|
|
9586
|
+
if (!dateTime) {
|
|
9587
|
+
const instant2 = instantFromStoreLocal(dateYYYYMMDD, 0, 0, 0, 0, timezone);
|
|
9588
|
+
return { status: "valid", value: { instant: instant2, normalized: instant2.toISOString() } };
|
|
9589
|
+
}
|
|
9590
|
+
const hour = Number(dateTime[4]);
|
|
9591
|
+
const minute = Number(dateTime[5]);
|
|
9592
|
+
const second = Number(dateTime[6] ?? "0");
|
|
9593
|
+
const millis = Number((dateTime[7] ?? "").slice(0, 3).padEnd(3, "0") || "0");
|
|
9594
|
+
if (hour > 23 || minute > 59 || second > 59) {
|
|
9595
|
+
return { status: "invalid", reason: `"${str}" has an out-of-range time` };
|
|
9596
|
+
}
|
|
9597
|
+
const offset = dateTime[8];
|
|
9598
|
+
if (!offset) {
|
|
9599
|
+
const instant2 = instantFromStoreLocal(dateYYYYMMDD, hour, minute, second, millis, timezone);
|
|
9600
|
+
return { status: "valid", value: { instant: instant2, normalized: instant2.toISOString() } };
|
|
9601
|
+
}
|
|
9602
|
+
let offsetMinutes = 0;
|
|
9603
|
+
if (offset !== "Z" && offset !== "z") {
|
|
9604
|
+
const offsetHourPart = Number(offset.slice(1, 3));
|
|
9605
|
+
const offsetMinutePart = Number(offset.slice(4, 6));
|
|
9606
|
+
offsetMinutes = (offset[0] === "-" ? -1 : 1) * (offsetHourPart * 60 + offsetMinutePart);
|
|
9607
|
+
if (offsetMinutes < MIN_OFFSET_MINUTES || offsetMinutes > MAX_OFFSET_MINUTES) {
|
|
9608
|
+
return {
|
|
9609
|
+
status: "invalid",
|
|
9610
|
+
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}`
|
|
9611
|
+
};
|
|
9612
|
+
}
|
|
9613
|
+
if (!VALID_OFFSET_MINUTE_PARTS.includes(offsetMinutePart)) {
|
|
9614
|
+
return {
|
|
9615
|
+
status: "invalid",
|
|
9616
|
+
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}`
|
|
9617
|
+
};
|
|
9618
|
+
}
|
|
9619
|
+
}
|
|
9620
|
+
const instant = new Date(
|
|
9621
|
+
Date.UTC(year, month - 1, day, hour, minute, second, millis) - offsetMinutes * 6e4
|
|
9622
|
+
);
|
|
9623
|
+
return { status: "valid", value: { instant, normalized: instant.toISOString() } };
|
|
9624
|
+
}
|
|
9625
|
+
function isRealCalendarDate(year, month, day) {
|
|
9626
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) return false;
|
|
9627
|
+
const probe = new Date(Date.UTC(year, month - 1, day));
|
|
9628
|
+
return probe.getUTCFullYear() === year && probe.getUTCMonth() === month - 1 && probe.getUTCDate() === day;
|
|
9629
|
+
}
|
|
9630
|
+
function instantFromStoreLocal(dateYYYYMMDD, hour, minute, second, millis, timezone) {
|
|
9631
|
+
const [year, month, day] = dateYYYYMMDD.split("-").map(Number);
|
|
9632
|
+
const naive = Date.UTC(year, month - 1, day, hour, minute, second, millis);
|
|
9633
|
+
const firstOffset = timezoneOffsetMs(naive, timezone);
|
|
9634
|
+
let instant = naive - firstOffset;
|
|
9635
|
+
const secondOffset = timezoneOffsetMs(instant, timezone);
|
|
9636
|
+
if (secondOffset !== firstOffset) instant = naive - secondOffset;
|
|
9637
|
+
return new Date(instant);
|
|
9638
|
+
}
|
|
9639
|
+
function timezoneOffsetMs(utcMillis, timezone) {
|
|
9640
|
+
let parts;
|
|
9641
|
+
try {
|
|
9642
|
+
parts = new Intl.DateTimeFormat("en-US", {
|
|
9643
|
+
timeZone: timezone,
|
|
9644
|
+
year: "numeric",
|
|
9645
|
+
month: "2-digit",
|
|
9646
|
+
day: "2-digit",
|
|
9647
|
+
hour: "2-digit",
|
|
9648
|
+
minute: "2-digit",
|
|
9649
|
+
second: "2-digit",
|
|
9650
|
+
hour12: false
|
|
9651
|
+
}).formatToParts(new Date(utcMillis));
|
|
9652
|
+
} catch {
|
|
9653
|
+
return 0;
|
|
9654
|
+
}
|
|
9655
|
+
const get = (type) => Number(parts.find((p) => p.type === type)?.value ?? "0");
|
|
9656
|
+
const hour = get("hour") === 24 ? 0 : get("hour");
|
|
9657
|
+
const asIfUtc = Date.UTC(get("year"), get("month") - 1, get("day"), hour, get("minute"), get("second"));
|
|
9658
|
+
return asIfUtc - Math.floor(utcMillis / 1e3) * 1e3;
|
|
9659
|
+
}
|
|
9267
9660
|
function isCalendarDateAllowed(dateYYYYMMDD, config) {
|
|
9268
9661
|
if (!config) return true;
|
|
9269
9662
|
if (config.minDate && dateYYYYMMDD < config.minDate) return false;
|
|
@@ -9291,6 +9684,12 @@ function computeAvailableSlots(config, dateYYYYMMDD) {
|
|
|
9291
9684
|
}
|
|
9292
9685
|
return slots;
|
|
9293
9686
|
}
|
|
9687
|
+
function getBusinessHoursForDate(config, dateYYYYMMDD) {
|
|
9688
|
+
if (!config?.businessHours?.length) return [];
|
|
9689
|
+
if (!isCalendarDateAllowed(dateYYYYMMDD, config)) return [];
|
|
9690
|
+
const weekday = weekdayOfDateString(dateYYYYMMDD);
|
|
9691
|
+
return config.businessHours.filter((w) => w.weekday === weekday);
|
|
9692
|
+
}
|
|
9294
9693
|
function isDateValueAllowed(instant, config, fieldType, timezone) {
|
|
9295
9694
|
if (!config) return { allowed: true };
|
|
9296
9695
|
if (fieldType === "DATE") {
|
|
@@ -9839,6 +10238,7 @@ export {
|
|
|
9839
10238
|
formatProductPrice,
|
|
9840
10239
|
formatVariantPrice,
|
|
9841
10240
|
getBlogSitemapEntries,
|
|
10241
|
+
getBusinessHoursForDate,
|
|
9842
10242
|
getCartItemImage,
|
|
9843
10243
|
getCartItemName,
|
|
9844
10244
|
getCartTotals,
|
|
@@ -9863,6 +10263,7 @@ export {
|
|
|
9863
10263
|
isHtmlDescription,
|
|
9864
10264
|
isWebhookEventType,
|
|
9865
10265
|
jsonLdScriptProps,
|
|
10266
|
+
parseDateFieldValue,
|
|
9866
10267
|
parseWebhookEvent,
|
|
9867
10268
|
resolveStoreLocalParts,
|
|
9868
10269
|
safePaymentRedirect,
|