brainerce 1.51.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 -5912
- 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 +405 -14
- package/dist/index.d.ts +405 -14
- package/dist/index.js +353 -25
- package/dist/index.mjs +351 -25
- 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.52.0";
|
|
119
119
|
|
|
120
120
|
// src/client.ts
|
|
121
121
|
var DEFAULT_BASE_URL = "https://api.brainerce.com";
|
|
@@ -171,6 +171,9 @@ 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;
|
|
174
177
|
/** localStorage key for session cart reference (sessionToken + cartId) */
|
|
175
178
|
this.SESSION_CART_KEY = "brainerce_session";
|
|
176
179
|
/**
|
|
@@ -1081,6 +1084,126 @@ var BrainerceClient = class {
|
|
|
1081
1084
|
} catch {
|
|
1082
1085
|
}
|
|
1083
1086
|
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Load GA4's `gtag.js` and start resolving the `client_id`/`session_id`
|
|
1089
|
+
* "stitch" ids Google needs to join a later server-side purchase conversion
|
|
1090
|
+
* to this browser's GA4 session (without them, a server-sent purchase event
|
|
1091
|
+
* either gets rejected or creates a phantom user in GA4). Call this once,
|
|
1092
|
+
* as early as possible (app entry / root layout).
|
|
1093
|
+
*
|
|
1094
|
+
* `createCart()`, `addToCart()`, `setCheckoutCustomer()`, and
|
|
1095
|
+
* `setShippingAddress()` all auto-attach the resolved ids once available —
|
|
1096
|
+
* an explicit `analyticsClientId`/`analyticsSessionId` you pass to any of
|
|
1097
|
+
* those always wins over the auto-captured value. You only need to call
|
|
1098
|
+
* this once; every subsequent cart/checkout call benefits automatically.
|
|
1099
|
+
*
|
|
1100
|
+
* Ids are resolved via `gtag('get', measurementId, 'client_id' | 'session_id', cb)`
|
|
1101
|
+
* — Google's documented method — never by parsing the `_ga` cookie, which
|
|
1102
|
+
* is undocumented and breaks silently across cookie-format changes and
|
|
1103
|
+
* Consent Mode v2 states. If the shopper has denied analytics consent,
|
|
1104
|
+
* gtag reports no client_id and the ids are simply omitted — never
|
|
1105
|
+
* synthesized.
|
|
1106
|
+
*
|
|
1107
|
+
* No-op outside the browser (SSR-safe) and never throws — a blocked or
|
|
1108
|
+
* slow gtag just means server-side conversions won't stitch; it never
|
|
1109
|
+
* breaks the storefront or delays checkout by more than `options.timeoutMs`.
|
|
1110
|
+
*
|
|
1111
|
+
* @example
|
|
1112
|
+
* ```typescript
|
|
1113
|
+
* // Call once, e.g. in your root layout / app entry point
|
|
1114
|
+
* client.loadGoogleAnalytics('G-XXXXXXX');
|
|
1115
|
+
*
|
|
1116
|
+
* // Every cart/checkout call from here on auto-forwards the stitch ids —
|
|
1117
|
+
* // no other code changes needed.
|
|
1118
|
+
* const cart = await client.createCart();
|
|
1119
|
+
* await client.addToCart(cart.id, { productId: 'prod_abc', quantity: 1 });
|
|
1120
|
+
* ```
|
|
1121
|
+
*/
|
|
1122
|
+
loadGoogleAnalytics(measurementId, options) {
|
|
1123
|
+
if (typeof window === "undefined" || !measurementId) return;
|
|
1124
|
+
if (this._ga4MeasurementId === measurementId && this._ga4StitchPromise) return;
|
|
1125
|
+
this._ga4MeasurementId = measurementId;
|
|
1126
|
+
try {
|
|
1127
|
+
if (!window.gtag) {
|
|
1128
|
+
window.dataLayer = window.dataLayer || [];
|
|
1129
|
+
const gtag = (...args) => {
|
|
1130
|
+
window.dataLayer.push(args);
|
|
1131
|
+
};
|
|
1132
|
+
window.gtag = gtag;
|
|
1133
|
+
gtag("js", /* @__PURE__ */ new Date());
|
|
1134
|
+
const script = document.createElement("script");
|
|
1135
|
+
script.async = true;
|
|
1136
|
+
script.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`;
|
|
1137
|
+
document.head.appendChild(script);
|
|
1138
|
+
}
|
|
1139
|
+
window.gtag("config", measurementId);
|
|
1140
|
+
} catch {
|
|
1141
|
+
}
|
|
1142
|
+
this._ga4StitchPromise = this.resolveGa4StitchIds(measurementId, options?.timeoutMs ?? 1500);
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Resolve GA4's `client_id`/`session_id` via `gtag('get', ...)`, bounded by
|
|
1146
|
+
* `timeoutMs` so a slow/blocked gtag never hangs a cart/checkout call.
|
|
1147
|
+
* Resolves to `{}` (never rejects) on timeout, missing gtag, or denied
|
|
1148
|
+
* consent.
|
|
1149
|
+
*/
|
|
1150
|
+
resolveGa4StitchIds(measurementId, timeoutMs) {
|
|
1151
|
+
return new Promise((resolve) => {
|
|
1152
|
+
if (typeof window === "undefined" || !window.gtag) {
|
|
1153
|
+
resolve({});
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
const result = {};
|
|
1157
|
+
let settled = false;
|
|
1158
|
+
const finish = () => {
|
|
1159
|
+
if (settled) return;
|
|
1160
|
+
settled = true;
|
|
1161
|
+
resolve(result);
|
|
1162
|
+
};
|
|
1163
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
1164
|
+
let pending = 2;
|
|
1165
|
+
const done = () => {
|
|
1166
|
+
pending -= 1;
|
|
1167
|
+
if (pending === 0) {
|
|
1168
|
+
clearTimeout(timer);
|
|
1169
|
+
finish();
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
try {
|
|
1173
|
+
window.gtag("get", measurementId, "client_id", (id) => {
|
|
1174
|
+
if (id) result.analyticsClientId = id;
|
|
1175
|
+
done();
|
|
1176
|
+
});
|
|
1177
|
+
window.gtag("get", measurementId, "session_id", (id) => {
|
|
1178
|
+
if (id) result.analyticsSessionId = String(id);
|
|
1179
|
+
done();
|
|
1180
|
+
});
|
|
1181
|
+
} catch {
|
|
1182
|
+
clearTimeout(timer);
|
|
1183
|
+
finish();
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Merge the resolved GA4 stitch ids onto a request body — only for fields
|
|
1189
|
+
* the caller didn't already set explicitly (explicit values always win).
|
|
1190
|
+
* No-op (returns `dto` unchanged) if `loadGoogleAnalytics()` was never
|
|
1191
|
+
* called, or if it hasn't resolved any ids by the time this is awaited.
|
|
1192
|
+
*/
|
|
1193
|
+
async withAnalyticsStitchIds(dto) {
|
|
1194
|
+
if (!this._ga4StitchPromise) return dto;
|
|
1195
|
+
try {
|
|
1196
|
+
const ids = await this._ga4StitchPromise;
|
|
1197
|
+
if (!ids.analyticsClientId && !ids.analyticsSessionId) return dto;
|
|
1198
|
+
return {
|
|
1199
|
+
...dto ?? {},
|
|
1200
|
+
analyticsClientId: dto?.analyticsClientId ?? ids.analyticsClientId,
|
|
1201
|
+
analyticsSessionId: dto?.analyticsSessionId ?? ids.analyticsSessionId
|
|
1202
|
+
};
|
|
1203
|
+
} catch {
|
|
1204
|
+
return dto;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1084
1207
|
// -------------------- Products --------------------
|
|
1085
1208
|
/**
|
|
1086
1209
|
* Get a list of products with pagination and filtering
|
|
@@ -1801,13 +1924,23 @@ var BrainerceClient = class {
|
|
|
1801
1924
|
}
|
|
1802
1925
|
/**
|
|
1803
1926
|
* 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
|
|
1927
|
+
* Pass the rate ID returned from rate shopping — treat it as opaque and never
|
|
1928
|
+
* parse it. Billing goes directly to the merchant's carrier account; Brainerce
|
|
1929
|
+
* is not a billing intermediary.
|
|
1930
|
+
*
|
|
1931
|
+
* `labelFormat` defaults to `PDF`. Use `ZPL` or `EPL` for warehouse thermal
|
|
1932
|
+
* printers. If the carrier cannot produce the requested format it returns its
|
|
1933
|
+
* closest match rather than failing the purchase — check the response.
|
|
1934
|
+
*
|
|
1935
|
+
* Once the label exists, tracking updates arrive automatically: the carrier's
|
|
1936
|
+
* webhooks move the shipment through in-transit → delivered and complete the
|
|
1937
|
+
* order. No polling required.
|
|
1806
1938
|
*
|
|
1807
1939
|
* @example
|
|
1808
1940
|
* ```typescript
|
|
1809
1941
|
* const label = await client.createShippingLabel('order_abc', {
|
|
1810
1942
|
* rateId: 'rate_8f123456789abcdef',
|
|
1943
|
+
* labelFormat: 'ZPL',
|
|
1811
1944
|
* });
|
|
1812
1945
|
* console.log('Label URL:', label.labelUrl);
|
|
1813
1946
|
* console.log('Tracking:', label.trackingNumber);
|
|
@@ -1820,6 +1953,37 @@ var BrainerceClient = class {
|
|
|
1820
1953
|
data
|
|
1821
1954
|
);
|
|
1822
1955
|
}
|
|
1956
|
+
/**
|
|
1957
|
+
* Live carrier rates for an order, from the merchant's installed shipping app.
|
|
1958
|
+
*
|
|
1959
|
+
* Call this immediately before {@link createShippingLabel} and pass the chosen
|
|
1960
|
+
* `id` straight through — the id is opaque and must not be parsed. It is also
|
|
1961
|
+
* short-lived: this call is what creates the shipment at the carrier, so a
|
|
1962
|
+
* rate from an old call may no longer be purchasable.
|
|
1963
|
+
*
|
|
1964
|
+
* Returns `[]` when the store has no shipping app installed, or when the
|
|
1965
|
+
* order has no usable shipping address.
|
|
1966
|
+
*
|
|
1967
|
+
* @example
|
|
1968
|
+
* ```typescript
|
|
1969
|
+
* const rates = await client.getOrderShippingRates('order_abc');
|
|
1970
|
+
* const cheapest = rates[0];
|
|
1971
|
+
* const label = await client.createShippingLabel('order_abc', { rateId: cheapest.id });
|
|
1972
|
+
* ```
|
|
1973
|
+
*/
|
|
1974
|
+
async getOrderShippingRates(orderId) {
|
|
1975
|
+
return this.request("GET", `/api/v1/orders/${encodePathSegment(orderId)}/shipments/app-rates`);
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Shipments recorded for an order, each with its tracking history.
|
|
1979
|
+
*
|
|
1980
|
+
* History arrives on its own: the carrier's webhooks flow through the
|
|
1981
|
+
* installed shipping app and append events as the parcel moves. Poll this for
|
|
1982
|
+
* display if you need it; do not poll expecting to *drive* anything.
|
|
1983
|
+
*/
|
|
1984
|
+
async getOrderShipments(orderId) {
|
|
1985
|
+
return this.request("GET", `/api/v1/orders/${encodePathSegment(orderId)}/shipments`);
|
|
1986
|
+
}
|
|
1823
1987
|
/**
|
|
1824
1988
|
* Cancel an order
|
|
1825
1989
|
* Works for Shopify and WooCommerce orders that haven't been fulfilled
|
|
@@ -1834,16 +1998,31 @@ var BrainerceClient = class {
|
|
|
1834
1998
|
return this.request("POST", `/api/v1/orders/${encodePathSegment(orderId)}/cancel`);
|
|
1835
1999
|
}
|
|
1836
2000
|
/**
|
|
1837
|
-
* Fulfill an order (mark as shipped)
|
|
1838
|
-
*
|
|
2001
|
+
* Fulfill an order (mark as shipped), or correct the tracking of an order
|
|
2002
|
+
* that has already shipped.
|
|
2003
|
+
*
|
|
2004
|
+
* Pass `trackingUrl` alongside the number — the shipped email only renders
|
|
2005
|
+
* its "Track Your Order" button when a URL is present.
|
|
2006
|
+
*
|
|
2007
|
+
* Calling this again on an order that is already `SHIPPED`/`FULFILLED` with
|
|
2008
|
+
* tracking fields edits only those fields: the status does not move, the
|
|
2009
|
+
* ship date is not rewritten, and no fulfilment event fires. That is the way
|
|
2010
|
+
* to fix a mistyped tracking number.
|
|
1839
2011
|
*
|
|
1840
2012
|
* @example
|
|
1841
2013
|
* ```typescript
|
|
1842
|
-
*
|
|
2014
|
+
* // First fulfilment — emails the shopper by default.
|
|
2015
|
+
* await client.fulfillOrder('order_123', {
|
|
1843
2016
|
* trackingNumber: '1Z999AA10123456784',
|
|
1844
2017
|
* trackingCompany: 'UPS',
|
|
2018
|
+
* trackingUrl: 'https://www.ups.com/track?tracknum=1Z999AA10123456784',
|
|
1845
2019
|
* notifyCustomer: true,
|
|
1846
2020
|
* });
|
|
2021
|
+
*
|
|
2022
|
+
* // Correction — silent unless you opt back in.
|
|
2023
|
+
* await client.fulfillOrder('order_123', {
|
|
2024
|
+
* trackingNumber: '1Z999AA10123456785',
|
|
2025
|
+
* });
|
|
1847
2026
|
* ```
|
|
1848
2027
|
*/
|
|
1849
2028
|
async fulfillOrder(orderId, data) {
|
|
@@ -2584,11 +2763,25 @@ var BrainerceClient = class {
|
|
|
2584
2763
|
*
|
|
2585
2764
|
* @param provider - OAuth provider ('GOOGLE', 'FACEBOOK', 'GITHUB')
|
|
2586
2765
|
* @param options - Optional configuration
|
|
2587
|
-
* @param options.redirectUrl -
|
|
2588
|
-
*
|
|
2589
|
-
*
|
|
2590
|
-
*
|
|
2591
|
-
*
|
|
2766
|
+
* @param options.redirectUrl - Where to send the browser once OAuth finishes —
|
|
2767
|
+
* on success *and* on failure. Validated server-side against the sales
|
|
2768
|
+
* channel's trusted origins, so what is accepted depends on the mode:
|
|
2769
|
+
* - vibe-coded (`salesChannelId: 'vc_*'`): an absolute URL on the channel's
|
|
2770
|
+
* registered `domain` or one of its `allowedOrigins`; in TEST mode, any
|
|
2771
|
+
* `localhost`/`127.0.0.1` port. A relative path (`/auth/callback`) also
|
|
2772
|
+
* works — it is resolved against the channel's `domain` on the way back,
|
|
2773
|
+
* so the channel must have one registered.
|
|
2774
|
+
* - storefront (`storeId`): **social login cannot round-trip in this mode.**
|
|
2775
|
+
* No channel is bound to the request, so an absolute URL has no
|
|
2776
|
+
* trusted-origin list to match (400 at this call) and a relative path has
|
|
2777
|
+
* no origin to resolve against on the way back. Use a `salesChannelId`
|
|
2778
|
+
* connection for OAuth.
|
|
2779
|
+
* Anything invalid fails fast here, before the shopper ever reaches the
|
|
2780
|
+
* provider.
|
|
2781
|
+
*
|
|
2782
|
+
* @example
|
|
2783
|
+
* ```typescript
|
|
2784
|
+
* // Vibe-coded mode — absolute URL on the registered storefront domain
|
|
2592
2785
|
* const { authorizationUrl } = await client.getOAuthAuthorizeUrl('GOOGLE', {
|
|
2593
2786
|
* redirectUrl: window.location.origin + '/auth/callback'
|
|
2594
2787
|
* });
|
|
@@ -2605,7 +2798,17 @@ var BrainerceClient = class {
|
|
|
2605
2798
|
* client.setCustomerToken(result.token);
|
|
2606
2799
|
* // result.customer, result.isNewCustomer, result.redirectUrl, ...
|
|
2607
2800
|
* } else if (params.get('oauth_error')) {
|
|
2608
|
-
* //
|
|
2801
|
+
* // Failures land on this same page, on `redirectUrl` — never on the API
|
|
2802
|
+
* // host. `oauth_error` is a stable snake_case code (see OAuthErrorCode);
|
|
2803
|
+
* // `error_description` is English developer detail, not shopper copy.
|
|
2804
|
+
* const code = params.get('oauth_error') as OAuthErrorCode;
|
|
2805
|
+
* showMessage(
|
|
2806
|
+
* code === 'link_blocked_unverified_password_account'
|
|
2807
|
+
* ? t('auth.verifyEmailFirst') // send them to email verification
|
|
2808
|
+
* : code === 'state_expired'
|
|
2809
|
+
* ? t('auth.sessionExpiredRetry')
|
|
2810
|
+
* : t('auth.signInFailed')
|
|
2811
|
+
* );
|
|
2609
2812
|
* }
|
|
2610
2813
|
* ```
|
|
2611
2814
|
*/
|
|
@@ -2981,6 +3184,9 @@ var BrainerceClient = class {
|
|
|
2981
3184
|
* Create a new cart for a guest user
|
|
2982
3185
|
* Returns a cart with a sessionToken that identifies this cart
|
|
2983
3186
|
*
|
|
3187
|
+
* If `loadGoogleAnalytics()` was called and has resolved a GA4 client/session
|
|
3188
|
+
* id, it's auto-attached unless `options` already specifies one.
|
|
3189
|
+
*
|
|
2984
3190
|
* @example
|
|
2985
3191
|
* ```typescript
|
|
2986
3192
|
* const cart = await client.createCart();
|
|
@@ -2988,14 +3194,15 @@ var BrainerceClient = class {
|
|
|
2988
3194
|
* // Store sessionToken in localStorage or cookie
|
|
2989
3195
|
* ```
|
|
2990
3196
|
*/
|
|
2991
|
-
async createCart() {
|
|
3197
|
+
async createCart(options) {
|
|
3198
|
+
const body = await this.withAnalyticsStitchIds(options);
|
|
2992
3199
|
if (this.isVibeCodedMode()) {
|
|
2993
|
-
return this.vibeCodedRequest("POST", "/cart");
|
|
3200
|
+
return this.vibeCodedRequest("POST", "/cart", body);
|
|
2994
3201
|
}
|
|
2995
3202
|
if (this.storeId && !this.apiKey) {
|
|
2996
|
-
return this.storefrontRequest("POST", "/cart");
|
|
3203
|
+
return this.storefrontRequest("POST", "/cart", body);
|
|
2997
3204
|
}
|
|
2998
|
-
return this.adminRequest("POST", "/api/v1/cart");
|
|
3205
|
+
return this.adminRequest("POST", "/api/v1/cart", body);
|
|
2999
3206
|
}
|
|
3000
3207
|
/**
|
|
3001
3208
|
* Get a cart by session token (for guest users)
|
|
@@ -3144,20 +3351,21 @@ var BrainerceClient = class {
|
|
|
3144
3351
|
});
|
|
3145
3352
|
return this.withGuards(this.localCartToCart(this.getLocalCart()), "cart");
|
|
3146
3353
|
}
|
|
3354
|
+
const body = await this.withAnalyticsStitchIds(item);
|
|
3147
3355
|
if (this.isVibeCodedMode()) {
|
|
3148
3356
|
return this.withGuards(
|
|
3149
|
-
this.vibeCodedRequest("POST", `/cart/${encodePathSegment(cartId)}/items`,
|
|
3357
|
+
this.vibeCodedRequest("POST", `/cart/${encodePathSegment(cartId)}/items`, body),
|
|
3150
3358
|
"cart"
|
|
3151
3359
|
);
|
|
3152
3360
|
}
|
|
3153
3361
|
if (this.storeId && !this.apiKey) {
|
|
3154
3362
|
return this.withGuards(
|
|
3155
|
-
this.storefrontRequest("POST", `/cart/${encodePathSegment(cartId)}/items`,
|
|
3363
|
+
this.storefrontRequest("POST", `/cart/${encodePathSegment(cartId)}/items`, body),
|
|
3156
3364
|
"cart"
|
|
3157
3365
|
);
|
|
3158
3366
|
}
|
|
3159
3367
|
return this.withGuards(
|
|
3160
|
-
this.adminRequest("POST", `/api/v1/cart/${encodePathSegment(cartId)}/items`,
|
|
3368
|
+
this.adminRequest("POST", `/api/v1/cart/${encodePathSegment(cartId)}/items`, body),
|
|
3161
3369
|
"cart"
|
|
3162
3370
|
);
|
|
3163
3371
|
}
|
|
@@ -4749,24 +4957,25 @@ var BrainerceClient = class {
|
|
|
4749
4957
|
* ```
|
|
4750
4958
|
*/
|
|
4751
4959
|
async setCheckoutCustomer(checkoutId, data) {
|
|
4960
|
+
const body = await this.withAnalyticsStitchIds(data);
|
|
4752
4961
|
if (this.isVibeCodedMode()) {
|
|
4753
4962
|
return this.vibeCodedRequest(
|
|
4754
4963
|
"PATCH",
|
|
4755
4964
|
`/checkout/${encodePathSegment(checkoutId)}/customer`,
|
|
4756
|
-
|
|
4965
|
+
body
|
|
4757
4966
|
);
|
|
4758
4967
|
}
|
|
4759
4968
|
if (this.storeId && !this.apiKey) {
|
|
4760
4969
|
return this.storefrontRequest(
|
|
4761
4970
|
"PATCH",
|
|
4762
4971
|
`/checkout/${encodePathSegment(checkoutId)}/customer`,
|
|
4763
|
-
|
|
4972
|
+
body
|
|
4764
4973
|
);
|
|
4765
4974
|
}
|
|
4766
4975
|
return this.adminRequest(
|
|
4767
4976
|
"PATCH",
|
|
4768
4977
|
`/api/v1/checkout/${encodePathSegment(checkoutId)}/customer`,
|
|
4769
|
-
|
|
4978
|
+
body
|
|
4770
4979
|
);
|
|
4771
4980
|
}
|
|
4772
4981
|
/**
|
|
@@ -4820,6 +5029,12 @@ var BrainerceClient = class {
|
|
|
4820
5029
|
* should include an optional "Order notes" textarea by default and send its
|
|
4821
5030
|
* value here (or via `setCheckoutCustomer`). The note lands on the order.
|
|
4822
5031
|
*
|
|
5032
|
+
* **Pass `placeId` whenever the address came from `addressAutocomplete()`.**
|
|
5033
|
+
* The server re-resolves it to exact coordinates and matches polygon
|
|
5034
|
+
* ("draw on map") shipping zones against those instead of re-geocoding the
|
|
5035
|
+
* address text — which is materially less accurate and can place the
|
|
5036
|
+
* shopper in a neighbouring city's zone, or in none at all.
|
|
5037
|
+
*
|
|
4823
5038
|
* @example
|
|
4824
5039
|
* ```typescript
|
|
4825
5040
|
* const { checkout, rates } = await client.setShippingAddress('checkout_123', {
|
|
@@ -4832,29 +5047,32 @@ var BrainerceClient = class {
|
|
|
4832
5047
|
* postalCode: '10001',
|
|
4833
5048
|
* country: 'US',
|
|
4834
5049
|
* notes: 'Please leave the package at the door', // optional order notes
|
|
5050
|
+
* placeId: suggestion.placeId, // from addressAutocomplete()
|
|
5051
|
+
* placeSessionToken: sessionToken, // the same token used for it
|
|
4835
5052
|
* });
|
|
4836
5053
|
* console.log('Available rates:', rates);
|
|
4837
5054
|
* ```
|
|
4838
5055
|
*/
|
|
4839
5056
|
async setShippingAddress(checkoutId, address) {
|
|
5057
|
+
const body = await this.withAnalyticsStitchIds(address);
|
|
4840
5058
|
if (this.isVibeCodedMode()) {
|
|
4841
5059
|
return this.vibeCodedRequest(
|
|
4842
5060
|
"PATCH",
|
|
4843
5061
|
`/checkout/${encodePathSegment(checkoutId)}/shipping-address`,
|
|
4844
|
-
|
|
5062
|
+
body
|
|
4845
5063
|
);
|
|
4846
5064
|
}
|
|
4847
5065
|
if (this.storeId && !this.apiKey) {
|
|
4848
5066
|
return this.storefrontRequest(
|
|
4849
5067
|
"PATCH",
|
|
4850
5068
|
`/checkout/${encodePathSegment(checkoutId)}/shipping-address`,
|
|
4851
|
-
|
|
5069
|
+
body
|
|
4852
5070
|
);
|
|
4853
5071
|
}
|
|
4854
5072
|
return this.adminRequest(
|
|
4855
5073
|
"PATCH",
|
|
4856
5074
|
`/api/v1/checkout/${encodePathSegment(checkoutId)}/shipping-address`,
|
|
4857
|
-
|
|
5075
|
+
body
|
|
4858
5076
|
);
|
|
4859
5077
|
}
|
|
4860
5078
|
/**
|
|
@@ -9164,6 +9382,11 @@ function formatAmount(amount, currency, locale) {
|
|
|
9164
9382
|
// src/date-availability.ts
|
|
9165
9383
|
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
9166
9384
|
var TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
|
|
9385
|
+
var DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
9386
|
+
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})?$/;
|
|
9387
|
+
var MIN_OFFSET_MINUTES = -12 * 60;
|
|
9388
|
+
var MAX_OFFSET_MINUTES = 14 * 60;
|
|
9389
|
+
var VALID_OFFSET_MINUTE_PARTS = [0, 30, 45];
|
|
9167
9390
|
function validateDateAvailabilityConfig(config, fieldType) {
|
|
9168
9391
|
const errors = [];
|
|
9169
9392
|
if (!config) return errors;
|
|
@@ -9264,6 +9487,101 @@ function resolveStoreLocalParts(instant, timezone) {
|
|
|
9264
9487
|
weekday: WEEKDAY_TO_INDEX[get("weekday")] ?? instant.getUTCDay()
|
|
9265
9488
|
};
|
|
9266
9489
|
}
|
|
9490
|
+
function parseDateFieldValue(raw, fieldType, timezone) {
|
|
9491
|
+
const str = raw instanceof Date ? Number.isNaN(raw.getTime()) ? "" : raw.toISOString() : typeof raw === "string" ? raw.trim() : "";
|
|
9492
|
+
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";
|
|
9493
|
+
if (!str) return { status: "invalid", reason: `"${String(raw)}" is not a valid date \u2014 ${expected}` };
|
|
9494
|
+
const dateOnly = DATE_ONLY_RE.exec(str);
|
|
9495
|
+
const dateTime = dateOnly ? null : DATE_TIME_RE.exec(str);
|
|
9496
|
+
const match = dateOnly ?? dateTime;
|
|
9497
|
+
if (!match) return { status: "invalid", reason: `"${str}" is not a valid date \u2014 ${expected}` };
|
|
9498
|
+
const year = Number(match[1]);
|
|
9499
|
+
const month = Number(match[2]);
|
|
9500
|
+
const day = Number(match[3]);
|
|
9501
|
+
if (!isRealCalendarDate(year, month, day)) {
|
|
9502
|
+
return { status: "invalid", reason: `"${str}" is not a real calendar date` };
|
|
9503
|
+
}
|
|
9504
|
+
const dateYYYYMMDD = `${match[1]}-${match[2]}-${match[3]}`;
|
|
9505
|
+
if (fieldType === "DATE") {
|
|
9506
|
+
return {
|
|
9507
|
+
status: "valid",
|
|
9508
|
+
value: { instant: /* @__PURE__ */ new Date(`${dateYYYYMMDD}T00:00:00.000Z`), normalized: dateYYYYMMDD }
|
|
9509
|
+
};
|
|
9510
|
+
}
|
|
9511
|
+
if (!dateTime) {
|
|
9512
|
+
const instant2 = instantFromStoreLocal(dateYYYYMMDD, 0, 0, 0, 0, timezone);
|
|
9513
|
+
return { status: "valid", value: { instant: instant2, normalized: instant2.toISOString() } };
|
|
9514
|
+
}
|
|
9515
|
+
const hour = Number(dateTime[4]);
|
|
9516
|
+
const minute = Number(dateTime[5]);
|
|
9517
|
+
const second = Number(dateTime[6] ?? "0");
|
|
9518
|
+
const millis = Number((dateTime[7] ?? "").slice(0, 3).padEnd(3, "0") || "0");
|
|
9519
|
+
if (hour > 23 || minute > 59 || second > 59) {
|
|
9520
|
+
return { status: "invalid", reason: `"${str}" has an out-of-range time` };
|
|
9521
|
+
}
|
|
9522
|
+
const offset = dateTime[8];
|
|
9523
|
+
if (!offset) {
|
|
9524
|
+
const instant2 = instantFromStoreLocal(dateYYYYMMDD, hour, minute, second, millis, timezone);
|
|
9525
|
+
return { status: "valid", value: { instant: instant2, normalized: instant2.toISOString() } };
|
|
9526
|
+
}
|
|
9527
|
+
let offsetMinutes = 0;
|
|
9528
|
+
if (offset !== "Z" && offset !== "z") {
|
|
9529
|
+
const offsetHourPart = Number(offset.slice(1, 3));
|
|
9530
|
+
const offsetMinutePart = Number(offset.slice(4, 6));
|
|
9531
|
+
offsetMinutes = (offset[0] === "-" ? -1 : 1) * (offsetHourPart * 60 + offsetMinutePart);
|
|
9532
|
+
if (offsetMinutes < MIN_OFFSET_MINUTES || offsetMinutes > MAX_OFFSET_MINUTES) {
|
|
9533
|
+
return {
|
|
9534
|
+
status: "invalid",
|
|
9535
|
+
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}`
|
|
9536
|
+
};
|
|
9537
|
+
}
|
|
9538
|
+
if (!VALID_OFFSET_MINUTE_PARTS.includes(offsetMinutePart)) {
|
|
9539
|
+
return {
|
|
9540
|
+
status: "invalid",
|
|
9541
|
+
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}`
|
|
9542
|
+
};
|
|
9543
|
+
}
|
|
9544
|
+
}
|
|
9545
|
+
const instant = new Date(
|
|
9546
|
+
Date.UTC(year, month - 1, day, hour, minute, second, millis) - offsetMinutes * 6e4
|
|
9547
|
+
);
|
|
9548
|
+
return { status: "valid", value: { instant, normalized: instant.toISOString() } };
|
|
9549
|
+
}
|
|
9550
|
+
function isRealCalendarDate(year, month, day) {
|
|
9551
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) return false;
|
|
9552
|
+
const probe = new Date(Date.UTC(year, month - 1, day));
|
|
9553
|
+
return probe.getUTCFullYear() === year && probe.getUTCMonth() === month - 1 && probe.getUTCDate() === day;
|
|
9554
|
+
}
|
|
9555
|
+
function instantFromStoreLocal(dateYYYYMMDD, hour, minute, second, millis, timezone) {
|
|
9556
|
+
const [year, month, day] = dateYYYYMMDD.split("-").map(Number);
|
|
9557
|
+
const naive = Date.UTC(year, month - 1, day, hour, minute, second, millis);
|
|
9558
|
+
const firstOffset = timezoneOffsetMs(naive, timezone);
|
|
9559
|
+
let instant = naive - firstOffset;
|
|
9560
|
+
const secondOffset = timezoneOffsetMs(instant, timezone);
|
|
9561
|
+
if (secondOffset !== firstOffset) instant = naive - secondOffset;
|
|
9562
|
+
return new Date(instant);
|
|
9563
|
+
}
|
|
9564
|
+
function timezoneOffsetMs(utcMillis, timezone) {
|
|
9565
|
+
let parts;
|
|
9566
|
+
try {
|
|
9567
|
+
parts = new Intl.DateTimeFormat("en-US", {
|
|
9568
|
+
timeZone: timezone,
|
|
9569
|
+
year: "numeric",
|
|
9570
|
+
month: "2-digit",
|
|
9571
|
+
day: "2-digit",
|
|
9572
|
+
hour: "2-digit",
|
|
9573
|
+
minute: "2-digit",
|
|
9574
|
+
second: "2-digit",
|
|
9575
|
+
hour12: false
|
|
9576
|
+
}).formatToParts(new Date(utcMillis));
|
|
9577
|
+
} catch {
|
|
9578
|
+
return 0;
|
|
9579
|
+
}
|
|
9580
|
+
const get = (type) => Number(parts.find((p) => p.type === type)?.value ?? "0");
|
|
9581
|
+
const hour = get("hour") === 24 ? 0 : get("hour");
|
|
9582
|
+
const asIfUtc = Date.UTC(get("year"), get("month") - 1, get("day"), hour, get("minute"), get("second"));
|
|
9583
|
+
return asIfUtc - Math.floor(utcMillis / 1e3) * 1e3;
|
|
9584
|
+
}
|
|
9267
9585
|
function isCalendarDateAllowed(dateYYYYMMDD, config) {
|
|
9268
9586
|
if (!config) return true;
|
|
9269
9587
|
if (config.minDate && dateYYYYMMDD < config.minDate) return false;
|
|
@@ -9291,6 +9609,12 @@ function computeAvailableSlots(config, dateYYYYMMDD) {
|
|
|
9291
9609
|
}
|
|
9292
9610
|
return slots;
|
|
9293
9611
|
}
|
|
9612
|
+
function getBusinessHoursForDate(config, dateYYYYMMDD) {
|
|
9613
|
+
if (!config?.businessHours?.length) return [];
|
|
9614
|
+
if (!isCalendarDateAllowed(dateYYYYMMDD, config)) return [];
|
|
9615
|
+
const weekday = weekdayOfDateString(dateYYYYMMDD);
|
|
9616
|
+
return config.businessHours.filter((w) => w.weekday === weekday);
|
|
9617
|
+
}
|
|
9294
9618
|
function isDateValueAllowed(instant, config, fieldType, timezone) {
|
|
9295
9619
|
if (!config) return { allowed: true };
|
|
9296
9620
|
if (fieldType === "DATE") {
|
|
@@ -9839,6 +10163,7 @@ export {
|
|
|
9839
10163
|
formatProductPrice,
|
|
9840
10164
|
formatVariantPrice,
|
|
9841
10165
|
getBlogSitemapEntries,
|
|
10166
|
+
getBusinessHoursForDate,
|
|
9842
10167
|
getCartItemImage,
|
|
9843
10168
|
getCartItemName,
|
|
9844
10169
|
getCartTotals,
|
|
@@ -9863,6 +10188,7 @@ export {
|
|
|
9863
10188
|
isHtmlDescription,
|
|
9864
10189
|
isWebhookEventType,
|
|
9865
10190
|
jsonLdScriptProps,
|
|
10191
|
+
parseDateFieldValue,
|
|
9866
10192
|
parseWebhookEvent,
|
|
9867
10193
|
resolveStoreLocalParts,
|
|
9868
10194
|
safePaymentRedirect,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainerce",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.52.0",
|
|
4
4
|
"description": "Official SDK for building e-commerce storefronts with Brainerce Platform. Perfect for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsup src/index.ts --format cjs,esm --dts && tsup src/bot/index.ts --format cjs,esm --dts --out-dir dist/bot && tsup src/bot/bootstrap.ts --format iife --minify --out-dir dist/bot",
|
|
26
|
+
"type-check": "tsc --noEmit",
|
|
26
27
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
27
28
|
"lint": "eslint \"src/**/*.ts\"",
|
|
28
29
|
"test": "vitest run",
|