brainerce 1.52.0 → 1.53.1
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 +72 -48
- package/dist/index.d.mts +92 -3
- package/dist/index.d.ts +92 -3
- package/dist/index.js +83 -8
- package/dist/index.mjs +83 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,9 +94,9 @@ Violating any of these causes production incidents or broken orders. Read them b
|
|
|
94
94
|
|
|
95
95
|
### Token handling
|
|
96
96
|
|
|
97
|
-
- Customer auth tokens (`result.token` from `loginCustomer`/`registerCustomer`) should be passed to `client.setCustomerToken(token)`. The SDK stores session state internally.
|
|
97
|
+
- Customer auth tokens (`result.token` from `loginCustomer`/`registerCustomer`) should be passed to `client.setCustomerToken(token)`. The SDK stores session state internally. `setCustomerToken` is a plain setter — always follow it with `await client.syncCartOnLogin()`, or the shopper's guest cart is never attached to their account and identity-keyed features (first-order discounts, per-customer usage caps, abandoned-cart recovery) misbehave.
|
|
98
98
|
- NEVER put the admin API key (`brainerce_*`) in client code. It is a server-only secret.
|
|
99
|
-
- OAuth callbacks arrive with a one-time `auth_code` URL param. Call `client.exchangeOAuthCode(authCode)` to swap it for the JWT
|
|
99
|
+
- OAuth callbacks arrive with a one-time `auth_code` URL param. Call `client.exchangeOAuthCode(authCode)` to swap it for the JWT, apply via `setCustomerToken`, then call `client.syncCartOnLogin()` to claim the guest cart. (The legacy `?token=` URL param is still emitted for backward compatibility but will be removed in the next major release.)
|
|
100
100
|
|
|
101
101
|
### i18n
|
|
102
102
|
|
|
@@ -167,9 +167,9 @@ These sequences are non-negotiable. The order of SDK calls matters.
|
|
|
167
167
|
```
|
|
168
168
|
3. Branch on `result.requiresVerification`:
|
|
169
169
|
- `true` → store token temporarily, route to verify-email UI (do NOT set token yet)
|
|
170
|
-
- `false` → `client.setCustomerToken(result.token)`, route to account
|
|
170
|
+
- `false` → `client.setCustomerToken(result.token)`, then `await client.syncCartOnLogin()`, route to account
|
|
171
171
|
4. On verify-email: collect 6-digit code → `client.verifyEmail(code)`. Offer resend via `client.resendVerificationEmail()`.
|
|
172
|
-
5. After `verifyEmail` resolves: `client.setCustomerToken(result.token)`, route to account.
|
|
172
|
+
5. After `verifyEmail` resolves: `client.setCustomerToken(result.token)`, then `await client.syncCartOnLogin()`, route to account.
|
|
173
173
|
|
|
174
174
|
> Build the verify-email step even if verification is currently disabled — it auto-hides.
|
|
175
175
|
|
|
@@ -182,7 +182,7 @@ These sequences are non-negotiable. The order of SDK calls matters.
|
|
|
182
182
|
```
|
|
183
183
|
3. Branch on `result.requiresVerification`:
|
|
184
184
|
- `true` → route to verify-email
|
|
185
|
-
- `false` → `client.setCustomerToken(result.token)`, route to previous page or account
|
|
185
|
+
- `false` → `client.setCustomerToken(result.token)`, then `await client.syncCartOnLogin()`, route to previous page or account
|
|
186
186
|
4. Always offer OAuth buttons from `client.getAvailableOAuthProviders()` — render the region even when empty, it auto-populates when a provider is enabled.
|
|
187
187
|
5. Render specific errors (bad credentials, rate limited, disabled) — never swallow them.
|
|
188
188
|
|
|
@@ -233,7 +233,9 @@ the credential, no customer token needed.
|
|
|
233
233
|
const code = params.get('auth_code');
|
|
234
234
|
if (code) {
|
|
235
235
|
const result = await client.exchangeOAuthCode(code);
|
|
236
|
-
client.setCustomerToken(result.token);
|
|
236
|
+
client.setCustomerToken(result.token);
|
|
237
|
+
await client.syncCartOnLogin(); // REQUIRED — claims the guest cart
|
|
238
|
+
// then redirect to account
|
|
237
239
|
}
|
|
238
240
|
```
|
|
239
241
|
The legacy `?token=` URL param is still emitted for backward compatibility but will be removed in the next major release — migrate to `auth_code` now.
|
|
@@ -717,6 +719,36 @@ const address: SetShippingAddressDto = {
|
|
|
717
719
|
};
|
|
718
720
|
```
|
|
719
721
|
|
|
722
|
+
**And no coordinates.** The address endpoints validate against a strict
|
|
723
|
+
allow-list — one property that isn't on the DTO rejects the whole call with
|
|
724
|
+
`400 "property lat should not exist"`, which doesn't degrade: it blocks the
|
|
725
|
+
address step for every shopper. `getAddressDetails()` resolves an address
|
|
726
|
+
carrying `lat`, `lng` and `formattedAddress`, so this is the one that bites:
|
|
727
|
+
|
|
728
|
+
```typescript
|
|
729
|
+
// ❌ WRONG — over raw HTTP, this 400s on every address
|
|
730
|
+
await fetch(`${base}/checkout/${id}/shipping-address`, {
|
|
731
|
+
method: 'PATCH',
|
|
732
|
+
body: JSON.stringify({ ...address, email, firstName, lastName }), // lat/lng ride along
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// ✅ CORRECT — the SDK (>= 1.53.0) strips lat/lng/formattedAddress for you,
|
|
736
|
+
// so the spread is safe here. Pass `placeId`: that is what reaches zone
|
|
737
|
+
// matching, since the server resolves the coordinates itself and never takes
|
|
738
|
+
// them from the client (zone matching decides which rate gets charged).
|
|
739
|
+
await client.setShippingAddress(checkoutId, {
|
|
740
|
+
...address,
|
|
741
|
+
email,
|
|
742
|
+
firstName,
|
|
743
|
+
lastName,
|
|
744
|
+
placeId: picked.placeId,
|
|
745
|
+
placeSessionToken: sessionToken,
|
|
746
|
+
});
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
Use `address.lat` / `address.lng` for your own UI — a map pin, a distance
|
|
750
|
+
readout — and nothing else.
|
|
751
|
+
|
|
720
752
|
### 10. OAuth - Use `authorizationUrl`, NOT `url`
|
|
721
753
|
|
|
722
754
|
```typescript
|
|
@@ -987,6 +1019,19 @@ const picked = suggestions[0];
|
|
|
987
1019
|
const { address, inZone } = await client.getAddressDetails(picked.placeId, sessionToken);
|
|
988
1020
|
// address: { line1, city, region, postalCode, country, lat, lng, formattedAddress }
|
|
989
1021
|
|
|
1022
|
+
// `country` can come back as an EMPTY STRING, and so can `region`. Google
|
|
1023
|
+
// omits the country entirely for places whose sovereignty it declines to
|
|
1024
|
+
// attribute — verified live for ordinary residential addresses such as Ramat
|
|
1025
|
+
// Shlomo, Giv'at Ze'ev, Modi'in Ilit and Ma'ale Adumim. The API deliberately
|
|
1026
|
+
// does not guess one. Ask the shopper to confirm the country in that case; the
|
|
1027
|
+
// rest of the address (line1, city, lat/lng, formattedAddress) is still good.
|
|
1028
|
+
// Note this also makes `inZone` read false when the store's zones are
|
|
1029
|
+
// country-listed — a polygon zone can still match, since the server matches
|
|
1030
|
+
// against the coordinates it resolves from `placeId`.
|
|
1031
|
+
if (!address.country) {
|
|
1032
|
+
// leave your country <select> for the shopper rather than sending ''
|
|
1033
|
+
}
|
|
1034
|
+
|
|
990
1035
|
// address.region is Google's own administrative-area code — usually (not
|
|
991
1036
|
// guaranteed) the same ISO 3166-2 subdivision code this store's own region
|
|
992
1037
|
// lists use. Validate against destinations.regions before trusting it;
|
|
@@ -995,12 +1040,19 @@ const destinations = await client.getShippingDestinations();
|
|
|
995
1040
|
const validRegions = destinations.regions[address.country] ?? [];
|
|
996
1041
|
const region = validRegions.some((r) => r.code === address.region) ? address.region : '';
|
|
997
1042
|
|
|
998
|
-
// Pass `placeId` through
|
|
999
|
-
//
|
|
1000
|
-
//
|
|
1001
|
-
//
|
|
1002
|
-
//
|
|
1003
|
-
//
|
|
1043
|
+
// Pass `placeId` through — that, not the coordinates, is what reaches zone
|
|
1044
|
+
// matching. The server never accepts `lat`/`lng` from the client, because zone
|
|
1045
|
+
// matching decides which shipping rate gets charged; it re-resolves the placeId
|
|
1046
|
+
// itself (cached, so no extra billed call) and matches map-drawn zones against
|
|
1047
|
+
// those exact coordinates instead of re-geocoding the text. Clear it if the
|
|
1048
|
+
// shopper then edits any address field.
|
|
1049
|
+
//
|
|
1050
|
+
// The `...address` spread below is safe even though `address` carries `lat`,
|
|
1051
|
+
// `lng` and `formattedAddress`: the SDK drops those three before sending. The
|
|
1052
|
+
// endpoint rejects unknown properties outright (`400 "property lat should not
|
|
1053
|
+
// exist"`), so if you call it over raw HTTP instead of the SDK, omit them
|
|
1054
|
+
// yourself. Use `address.lat`/`address.lng` for your own UI — a map pin, a
|
|
1055
|
+
// distance readout — and nothing else.
|
|
1004
1056
|
await client.setShippingAddress(checkout.id, {
|
|
1005
1057
|
firstName: 'John',
|
|
1006
1058
|
lastName: 'Doe',
|
|
@@ -2417,7 +2469,14 @@ console.log(rates); // ShippingRate[]
|
|
|
2417
2469
|
> address field after picking** — its coordinates describe the suggestion, not
|
|
2418
2470
|
> the edited text. There is deliberately no `lat`/`lng` field: zone matching
|
|
2419
2471
|
> decides which rate is charged, so coordinates are never accepted from the
|
|
2420
|
-
> client.
|
|
2472
|
+
> client — the server resolves them from `placeId` itself.
|
|
2473
|
+
>
|
|
2474
|
+
> The endpoint rejects **any** unknown property with a `400` ("property lat
|
|
2475
|
+
> should not exist"), which blocks checkout entirely rather than degrading. So
|
|
2476
|
+
> that spreading `getAddressDetails().address` in here can't do that, the SDK
|
|
2477
|
+
> drops its `lat`, `lng` and `formattedAddress` before sending (once per client
|
|
2478
|
+
> it logs a `console.warn` saying so). Calling the REST endpoint directly
|
|
2479
|
+
> instead of through the SDK? Then omit those three yourself.
|
|
2421
2480
|
|
|
2422
2481
|
> **One zone per address:** an address matches exactly ONE shipping zone and you
|
|
2423
2482
|
> get only that zone's rates. When zones overlap, the lower `priority` wins;
|
|
@@ -4512,41 +4571,6 @@ const { region: resolved } = await store.getAutoRegion('DE'); // server-side, on
|
|
|
4512
4571
|
> otherwise the checkout is charged in the store base currency. See the Checkout
|
|
4513
4572
|
> section.
|
|
4514
4573
|
|
|
4515
|
-
### Price Lists (per-region pricing)
|
|
4516
|
-
|
|
4517
|
-
Each region has a **price list** — entries that override a product's or variant's
|
|
4518
|
-
catalog price for buyers in that region. Requires `price-lists:read` /
|
|
4519
|
-
`price-lists:write`. Prices are in the region's currency.
|
|
4520
|
-
|
|
4521
|
-
> ⚠️ **Data-entry only for now.** Region prices are **not yet applied at checkout** —
|
|
4522
|
-
> charging a region-currency price through the store-currency cart needs FX
|
|
4523
|
-
> conversion (a later phase). These methods let you populate the price book ahead of
|
|
4524
|
-
> activation; until then checkout uses the catalog price.
|
|
4525
|
-
|
|
4526
|
-
```typescript
|
|
4527
|
-
// One entry per product OR variant (variant wins). price > 0; compareAtPrice optional.
|
|
4528
|
-
const res = await client.upsertPriceListEntries('region_eu', [
|
|
4529
|
-
{ productId: 'prod_tshirt', price: 27, compareAtPrice: 32 },
|
|
4530
|
-
{ variantId: 'var_tshirt_l', price: 29 },
|
|
4531
|
-
]);
|
|
4532
|
-
res.upserted; // 2
|
|
4533
|
-
res.warnings; // e.g. ["PL-004: compareAtPrice not greater than price …"] (non-blocking)
|
|
4534
|
-
|
|
4535
|
-
// Inspect
|
|
4536
|
-
const meta = await client.getPriceList('region_eu'); // { currency, entryCount, … }
|
|
4537
|
-
const { data: entries } = await client.listPriceListEntries('region_eu', {
|
|
4538
|
-
productId: 'prod_tshirt',
|
|
4539
|
-
});
|
|
4540
|
-
|
|
4541
|
-
// Bulk CSV (columns: productSku,variantSku?,price,compareAtPrice?). Unknown SKUs /
|
|
4542
|
-
// bad prices are skipped + reported, not fatal.
|
|
4543
|
-
const imported = await client.importPrices('region_eu', csvString);
|
|
4544
|
-
imported.skipped; // ['row 4: product SKU "NOPE" not found', …]
|
|
4545
|
-
const csv = await client.exportPrices('region_eu'); // round-trips with importPrices
|
|
4546
|
-
|
|
4547
|
-
await client.deletePriceListEntry('region_eu', 'entry_id'); // line falls back to catalog
|
|
4548
|
-
```
|
|
4549
|
-
|
|
4550
4574
|
### Metafield Definitions
|
|
4551
4575
|
|
|
4552
4576
|
```typescript
|
package/dist/index.d.mts
CHANGED
|
@@ -3188,8 +3188,12 @@ interface SetShippingAddressDto {
|
|
|
3188
3188
|
* which is the correct behaviour once the two no longer agree.
|
|
3189
3189
|
*
|
|
3190
3190
|
* Note there is deliberately no `lat`/`lng` field: zone matching decides
|
|
3191
|
-
* which shipping rate is offered and charged, so
|
|
3192
|
-
*
|
|
3191
|
+
* which shipping rate is offered and charged, so the server resolves the
|
|
3192
|
+
* coordinates itself from this `placeId` and never accepts them from the
|
|
3193
|
+
* client. You can still spread `getAddressDetails().address` in here — the
|
|
3194
|
+
* SDK drops its `lat`/`lng`/`formattedAddress` before sending, because the
|
|
3195
|
+
* endpoint rejects any unknown property with a `400` that would otherwise
|
|
3196
|
+
* block checkout entirely.
|
|
3193
3197
|
*/
|
|
3194
3198
|
placeId?: string;
|
|
3195
3199
|
/**
|
|
@@ -3288,11 +3292,44 @@ interface AddressDetailsResult {
|
|
|
3288
3292
|
*/
|
|
3289
3293
|
region: string;
|
|
3290
3294
|
postalCode: string;
|
|
3295
|
+
/**
|
|
3296
|
+
* ISO-3166-1 alpha-2, or **an empty string** — Google omits the country
|
|
3297
|
+
* outright for places whose sovereignty it declines to attribute, which in
|
|
3298
|
+
* practice includes ordinary residential addresses (verified live: Ramat
|
|
3299
|
+
* Shlomo, Giv'at Ze'ev, Modi'in Ilit, Ma'ale Adumim all come back with no
|
|
3300
|
+
* country). The API deliberately does not guess one.
|
|
3301
|
+
*
|
|
3302
|
+
* Treat it exactly like `region`: when it's empty, leave your country
|
|
3303
|
+
* field for the shopper to confirm rather than submitting a blank
|
|
3304
|
+
* `SetShippingAddressDto.country`. Everything else on the address
|
|
3305
|
+
* (`line1`, `city`, `lat`/`lng`, `formattedAddress`) is still valid and
|
|
3306
|
+
* should still be filled in.
|
|
3307
|
+
*
|
|
3308
|
+
* Note that `region` is necessarily empty too whenever this is — region
|
|
3309
|
+
* codes are resolved *within* a country, so with no country there is no
|
|
3310
|
+
* region list to match against. Prompt for both.
|
|
3311
|
+
*/
|
|
3291
3312
|
country: string;
|
|
3313
|
+
/**
|
|
3314
|
+
* Resolved coordinates, **for your own use only** — a map pin, a distance
|
|
3315
|
+
* readout. They are not part of any address payload: zone matching decides
|
|
3316
|
+
* which shipping rate is offered and charged, so the server re-resolves
|
|
3317
|
+
* them from `placeId` instead of trusting the client. Sending them is
|
|
3318
|
+
* harmless (the SDK strips `lat`/`lng`/`formattedAddress` from
|
|
3319
|
+
* `setShippingAddress()` / `setBillingAddress()` bodies), but pass
|
|
3320
|
+
* `placeId` — that is what actually reaches zone matching.
|
|
3321
|
+
*/
|
|
3292
3322
|
lat: number;
|
|
3293
3323
|
lng: number;
|
|
3294
3324
|
formattedAddress: string;
|
|
3295
3325
|
};
|
|
3326
|
+
/**
|
|
3327
|
+
* Soft coverage hint, never a rejection. Note it can read `false` purely
|
|
3328
|
+
* because `country` came back empty and the store's zones are country-listed
|
|
3329
|
+
* — a polygon zone can still match this address, since the server matches it
|
|
3330
|
+
* against the coordinates it resolves from `placeId`. Show a "we'll confirm
|
|
3331
|
+
* by phone" banner; do not block the shopper.
|
|
3332
|
+
*/
|
|
3296
3333
|
inZone: boolean;
|
|
3297
3334
|
}
|
|
3298
3335
|
interface CompleteCheckoutResponse {
|
|
@@ -6303,6 +6340,14 @@ declare class BrainerceClient {
|
|
|
6303
6340
|
private _pendingRecoverCartId;
|
|
6304
6341
|
private _ga4MeasurementId;
|
|
6305
6342
|
private _ga4StitchPromise;
|
|
6343
|
+
/**
|
|
6344
|
+
* Fields present on `getAddressDetails().address` that the address endpoints
|
|
6345
|
+
* do NOT accept — stripped by `stripResolvedOnlyAddressFields()` so a
|
|
6346
|
+
* `{ ...address }` spread doesn't 400 the whole checkout.
|
|
6347
|
+
*/
|
|
6348
|
+
private static readonly RESOLVED_ONLY_ADDRESS_FIELDS;
|
|
6349
|
+
/** One warning per client, not per keystroke-driven address submit. */
|
|
6350
|
+
private _warnedResolvedOnlyAddressFields;
|
|
6306
6351
|
/** localStorage key for session cart reference (sessionToken + cartId) */
|
|
6307
6352
|
private readonly SESSION_CART_KEY;
|
|
6308
6353
|
/**
|
|
@@ -6376,10 +6421,19 @@ declare class BrainerceClient {
|
|
|
6376
6421
|
* Set the customer authentication token (obtained from login/register).
|
|
6377
6422
|
* Required for accessing customer-specific data in storefront mode.
|
|
6378
6423
|
*
|
|
6424
|
+
* This is a plain setter — it authenticates subsequent requests and nothing
|
|
6425
|
+
* else. In particular it does NOT attach the shopper's existing guest cart to
|
|
6426
|
+
* their account. Pair every sign-in with {@link syncCartOnLogin}, or the cart
|
|
6427
|
+
* stays anonymous and every feature keyed on buyer identity degrades quietly:
|
|
6428
|
+
* "first order only" discounts keep applying to returning customers,
|
|
6429
|
+
* per-customer usage caps stop being enforced at cart time, and abandoned-cart
|
|
6430
|
+
* recovery can't identify who to email.
|
|
6431
|
+
*
|
|
6379
6432
|
* @example
|
|
6380
6433
|
* ```typescript
|
|
6381
6434
|
* const auth = await client.loginCustomer('user@example.com', 'password');
|
|
6382
6435
|
* client.setCustomerToken(auth.token);
|
|
6436
|
+
* await client.syncCartOnLogin(); // claim the guest cart for this account
|
|
6383
6437
|
*
|
|
6384
6438
|
* // Now can access customer data
|
|
6385
6439
|
* const profile = await client.getMyProfile();
|
|
@@ -6525,6 +6579,24 @@ declare class BrainerceClient {
|
|
|
6525
6579
|
* called, or if it hasn't resolved any ids by the time this is awaited.
|
|
6526
6580
|
*/
|
|
6527
6581
|
private withAnalyticsStitchIds;
|
|
6582
|
+
/**
|
|
6583
|
+
* Drop the fields `getAddressDetails()` returns that no address endpoint
|
|
6584
|
+
* accepts, so spreading its `address` straight into `setShippingAddress()`
|
|
6585
|
+
* / `setBillingAddress()` works instead of failing the whole request.
|
|
6586
|
+
*
|
|
6587
|
+
* The address endpoints validate against a strict allow-list: ONE unknown
|
|
6588
|
+
* property rejects the call with `400 "property lat should not exist"`, and
|
|
6589
|
+
* the shopper cannot check out at all. `lat`/`lng`/`formattedAddress` are
|
|
6590
|
+
* the only realistic way to hit that — they come out of this SDK's own
|
|
6591
|
+
* resolved-address shape, so this SDK cleans up after itself rather than
|
|
6592
|
+
* making every storefront remember to. Nothing else is stripped: a genuine
|
|
6593
|
+
* typo still reaches the server and still fails loudly.
|
|
6594
|
+
*
|
|
6595
|
+
* Coordinates are dropped rather than forwarded because zone matching picks
|
|
6596
|
+
* which shipping rate is offered and charged — the server resolves them
|
|
6597
|
+
* itself from `placeId`, and never takes them from the caller.
|
|
6598
|
+
*/
|
|
6599
|
+
private stripResolvedOnlyAddressFields;
|
|
6528
6600
|
/**
|
|
6529
6601
|
* Get a list of products with pagination and filtering
|
|
6530
6602
|
* Works in vibe-coded, storefront (public), and admin mode
|
|
@@ -7643,6 +7715,12 @@ declare class BrainerceClient {
|
|
|
7643
7715
|
* if (params.get('oauth_success') === 'true' && params.get('auth_code')) {
|
|
7644
7716
|
* const result = await client.exchangeOAuthCode(params.get('auth_code')!);
|
|
7645
7717
|
* client.setCustomerToken(result.token);
|
|
7718
|
+
* // REQUIRED: setCustomerToken only stores the JWT — it does NOT attach the
|
|
7719
|
+
* // guest cart to the account. Without this call the cart stays anonymous,
|
|
7720
|
+
* // and anything keyed on the buyer's identity misbehaves: "first order
|
|
7721
|
+
* // only" discounts re-apply to returning customers, per-customer usage
|
|
7722
|
+
* // caps go unenforced, and abandoned-cart recovery can't reach them.
|
|
7723
|
+
* await client.syncCartOnLogin();
|
|
7646
7724
|
* // result.customer, result.isNewCustomer, result.redirectUrl, ...
|
|
7647
7725
|
* } else if (params.get('oauth_error')) {
|
|
7648
7726
|
* // Failures land on this same page, on `redirectUrl` — never on the API
|
|
@@ -7677,6 +7755,11 @@ declare class BrainerceClient {
|
|
|
7677
7755
|
*
|
|
7678
7756
|
* @param authCode - The single-use code from the `?auth_code=` URL param.
|
|
7679
7757
|
*
|
|
7758
|
+
* Always follow a successful exchange with `syncCartOnLogin()`. Storing the
|
|
7759
|
+
* token does not claim the guest cart, and an unclaimed cart has no buyer
|
|
7760
|
+
* identity — which silently breaks first-order discounts, per-customer usage
|
|
7761
|
+
* caps, and abandoned-cart recovery for everyone who signs in with OAuth.
|
|
7762
|
+
*
|
|
7680
7763
|
* @example
|
|
7681
7764
|
* ```typescript
|
|
7682
7765
|
* const params = new URLSearchParams(window.location.search);
|
|
@@ -7685,6 +7768,7 @@ declare class BrainerceClient {
|
|
|
7685
7768
|
* const { token, customer, isNewCustomer, redirectUrl } =
|
|
7686
7769
|
* await client.exchangeOAuthCode(code);
|
|
7687
7770
|
* client.setCustomerToken(token);
|
|
7771
|
+
* await client.syncCartOnLogin(); // attach the guest cart to the account
|
|
7688
7772
|
* }
|
|
7689
7773
|
* ```
|
|
7690
7774
|
*/
|
|
@@ -8840,6 +8924,11 @@ declare class BrainerceClient {
|
|
|
8840
8924
|
* address text — which is materially less accurate and can place the
|
|
8841
8925
|
* shopper in a neighbouring city's zone, or in none at all.
|
|
8842
8926
|
*
|
|
8927
|
+
* Spreading `getAddressDetails().address` in here is safe: its `lat`, `lng`
|
|
8928
|
+
* and `formattedAddress` are dropped before the request goes out (the
|
|
8929
|
+
* endpoint rejects unknown properties outright, and coordinates are never
|
|
8930
|
+
* taken from the client — the server resolves them from `placeId`).
|
|
8931
|
+
*
|
|
8843
8932
|
* @example
|
|
8844
8933
|
* ```typescript
|
|
8845
8934
|
* const { checkout, rates } = await client.setShippingAddress('checkout_123', {
|
|
@@ -11037,7 +11126,7 @@ declare class BrainerceError extends Error {
|
|
|
11037
11126
|
constructor(message: string, statusCode: number, details?: unknown);
|
|
11038
11127
|
}
|
|
11039
11128
|
|
|
11040
|
-
declare const SDK_VERSION = "1.
|
|
11129
|
+
declare const SDK_VERSION = "1.53.1";
|
|
11041
11130
|
|
|
11042
11131
|
/**
|
|
11043
11132
|
* Verify a webhook signature from Brainerce
|
package/dist/index.d.ts
CHANGED
|
@@ -3188,8 +3188,12 @@ interface SetShippingAddressDto {
|
|
|
3188
3188
|
* which is the correct behaviour once the two no longer agree.
|
|
3189
3189
|
*
|
|
3190
3190
|
* Note there is deliberately no `lat`/`lng` field: zone matching decides
|
|
3191
|
-
* which shipping rate is offered and charged, so
|
|
3192
|
-
*
|
|
3191
|
+
* which shipping rate is offered and charged, so the server resolves the
|
|
3192
|
+
* coordinates itself from this `placeId` and never accepts them from the
|
|
3193
|
+
* client. You can still spread `getAddressDetails().address` in here — the
|
|
3194
|
+
* SDK drops its `lat`/`lng`/`formattedAddress` before sending, because the
|
|
3195
|
+
* endpoint rejects any unknown property with a `400` that would otherwise
|
|
3196
|
+
* block checkout entirely.
|
|
3193
3197
|
*/
|
|
3194
3198
|
placeId?: string;
|
|
3195
3199
|
/**
|
|
@@ -3288,11 +3292,44 @@ interface AddressDetailsResult {
|
|
|
3288
3292
|
*/
|
|
3289
3293
|
region: string;
|
|
3290
3294
|
postalCode: string;
|
|
3295
|
+
/**
|
|
3296
|
+
* ISO-3166-1 alpha-2, or **an empty string** — Google omits the country
|
|
3297
|
+
* outright for places whose sovereignty it declines to attribute, which in
|
|
3298
|
+
* practice includes ordinary residential addresses (verified live: Ramat
|
|
3299
|
+
* Shlomo, Giv'at Ze'ev, Modi'in Ilit, Ma'ale Adumim all come back with no
|
|
3300
|
+
* country). The API deliberately does not guess one.
|
|
3301
|
+
*
|
|
3302
|
+
* Treat it exactly like `region`: when it's empty, leave your country
|
|
3303
|
+
* field for the shopper to confirm rather than submitting a blank
|
|
3304
|
+
* `SetShippingAddressDto.country`. Everything else on the address
|
|
3305
|
+
* (`line1`, `city`, `lat`/`lng`, `formattedAddress`) is still valid and
|
|
3306
|
+
* should still be filled in.
|
|
3307
|
+
*
|
|
3308
|
+
* Note that `region` is necessarily empty too whenever this is — region
|
|
3309
|
+
* codes are resolved *within* a country, so with no country there is no
|
|
3310
|
+
* region list to match against. Prompt for both.
|
|
3311
|
+
*/
|
|
3291
3312
|
country: string;
|
|
3313
|
+
/**
|
|
3314
|
+
* Resolved coordinates, **for your own use only** — a map pin, a distance
|
|
3315
|
+
* readout. They are not part of any address payload: zone matching decides
|
|
3316
|
+
* which shipping rate is offered and charged, so the server re-resolves
|
|
3317
|
+
* them from `placeId` instead of trusting the client. Sending them is
|
|
3318
|
+
* harmless (the SDK strips `lat`/`lng`/`formattedAddress` from
|
|
3319
|
+
* `setShippingAddress()` / `setBillingAddress()` bodies), but pass
|
|
3320
|
+
* `placeId` — that is what actually reaches zone matching.
|
|
3321
|
+
*/
|
|
3292
3322
|
lat: number;
|
|
3293
3323
|
lng: number;
|
|
3294
3324
|
formattedAddress: string;
|
|
3295
3325
|
};
|
|
3326
|
+
/**
|
|
3327
|
+
* Soft coverage hint, never a rejection. Note it can read `false` purely
|
|
3328
|
+
* because `country` came back empty and the store's zones are country-listed
|
|
3329
|
+
* — a polygon zone can still match this address, since the server matches it
|
|
3330
|
+
* against the coordinates it resolves from `placeId`. Show a "we'll confirm
|
|
3331
|
+
* by phone" banner; do not block the shopper.
|
|
3332
|
+
*/
|
|
3296
3333
|
inZone: boolean;
|
|
3297
3334
|
}
|
|
3298
3335
|
interface CompleteCheckoutResponse {
|
|
@@ -6303,6 +6340,14 @@ declare class BrainerceClient {
|
|
|
6303
6340
|
private _pendingRecoverCartId;
|
|
6304
6341
|
private _ga4MeasurementId;
|
|
6305
6342
|
private _ga4StitchPromise;
|
|
6343
|
+
/**
|
|
6344
|
+
* Fields present on `getAddressDetails().address` that the address endpoints
|
|
6345
|
+
* do NOT accept — stripped by `stripResolvedOnlyAddressFields()` so a
|
|
6346
|
+
* `{ ...address }` spread doesn't 400 the whole checkout.
|
|
6347
|
+
*/
|
|
6348
|
+
private static readonly RESOLVED_ONLY_ADDRESS_FIELDS;
|
|
6349
|
+
/** One warning per client, not per keystroke-driven address submit. */
|
|
6350
|
+
private _warnedResolvedOnlyAddressFields;
|
|
6306
6351
|
/** localStorage key for session cart reference (sessionToken + cartId) */
|
|
6307
6352
|
private readonly SESSION_CART_KEY;
|
|
6308
6353
|
/**
|
|
@@ -6376,10 +6421,19 @@ declare class BrainerceClient {
|
|
|
6376
6421
|
* Set the customer authentication token (obtained from login/register).
|
|
6377
6422
|
* Required for accessing customer-specific data in storefront mode.
|
|
6378
6423
|
*
|
|
6424
|
+
* This is a plain setter — it authenticates subsequent requests and nothing
|
|
6425
|
+
* else. In particular it does NOT attach the shopper's existing guest cart to
|
|
6426
|
+
* their account. Pair every sign-in with {@link syncCartOnLogin}, or the cart
|
|
6427
|
+
* stays anonymous and every feature keyed on buyer identity degrades quietly:
|
|
6428
|
+
* "first order only" discounts keep applying to returning customers,
|
|
6429
|
+
* per-customer usage caps stop being enforced at cart time, and abandoned-cart
|
|
6430
|
+
* recovery can't identify who to email.
|
|
6431
|
+
*
|
|
6379
6432
|
* @example
|
|
6380
6433
|
* ```typescript
|
|
6381
6434
|
* const auth = await client.loginCustomer('user@example.com', 'password');
|
|
6382
6435
|
* client.setCustomerToken(auth.token);
|
|
6436
|
+
* await client.syncCartOnLogin(); // claim the guest cart for this account
|
|
6383
6437
|
*
|
|
6384
6438
|
* // Now can access customer data
|
|
6385
6439
|
* const profile = await client.getMyProfile();
|
|
@@ -6525,6 +6579,24 @@ declare class BrainerceClient {
|
|
|
6525
6579
|
* called, or if it hasn't resolved any ids by the time this is awaited.
|
|
6526
6580
|
*/
|
|
6527
6581
|
private withAnalyticsStitchIds;
|
|
6582
|
+
/**
|
|
6583
|
+
* Drop the fields `getAddressDetails()` returns that no address endpoint
|
|
6584
|
+
* accepts, so spreading its `address` straight into `setShippingAddress()`
|
|
6585
|
+
* / `setBillingAddress()` works instead of failing the whole request.
|
|
6586
|
+
*
|
|
6587
|
+
* The address endpoints validate against a strict allow-list: ONE unknown
|
|
6588
|
+
* property rejects the call with `400 "property lat should not exist"`, and
|
|
6589
|
+
* the shopper cannot check out at all. `lat`/`lng`/`formattedAddress` are
|
|
6590
|
+
* the only realistic way to hit that — they come out of this SDK's own
|
|
6591
|
+
* resolved-address shape, so this SDK cleans up after itself rather than
|
|
6592
|
+
* making every storefront remember to. Nothing else is stripped: a genuine
|
|
6593
|
+
* typo still reaches the server and still fails loudly.
|
|
6594
|
+
*
|
|
6595
|
+
* Coordinates are dropped rather than forwarded because zone matching picks
|
|
6596
|
+
* which shipping rate is offered and charged — the server resolves them
|
|
6597
|
+
* itself from `placeId`, and never takes them from the caller.
|
|
6598
|
+
*/
|
|
6599
|
+
private stripResolvedOnlyAddressFields;
|
|
6528
6600
|
/**
|
|
6529
6601
|
* Get a list of products with pagination and filtering
|
|
6530
6602
|
* Works in vibe-coded, storefront (public), and admin mode
|
|
@@ -7643,6 +7715,12 @@ declare class BrainerceClient {
|
|
|
7643
7715
|
* if (params.get('oauth_success') === 'true' && params.get('auth_code')) {
|
|
7644
7716
|
* const result = await client.exchangeOAuthCode(params.get('auth_code')!);
|
|
7645
7717
|
* client.setCustomerToken(result.token);
|
|
7718
|
+
* // REQUIRED: setCustomerToken only stores the JWT — it does NOT attach the
|
|
7719
|
+
* // guest cart to the account. Without this call the cart stays anonymous,
|
|
7720
|
+
* // and anything keyed on the buyer's identity misbehaves: "first order
|
|
7721
|
+
* // only" discounts re-apply to returning customers, per-customer usage
|
|
7722
|
+
* // caps go unenforced, and abandoned-cart recovery can't reach them.
|
|
7723
|
+
* await client.syncCartOnLogin();
|
|
7646
7724
|
* // result.customer, result.isNewCustomer, result.redirectUrl, ...
|
|
7647
7725
|
* } else if (params.get('oauth_error')) {
|
|
7648
7726
|
* // Failures land on this same page, on `redirectUrl` — never on the API
|
|
@@ -7677,6 +7755,11 @@ declare class BrainerceClient {
|
|
|
7677
7755
|
*
|
|
7678
7756
|
* @param authCode - The single-use code from the `?auth_code=` URL param.
|
|
7679
7757
|
*
|
|
7758
|
+
* Always follow a successful exchange with `syncCartOnLogin()`. Storing the
|
|
7759
|
+
* token does not claim the guest cart, and an unclaimed cart has no buyer
|
|
7760
|
+
* identity — which silently breaks first-order discounts, per-customer usage
|
|
7761
|
+
* caps, and abandoned-cart recovery for everyone who signs in with OAuth.
|
|
7762
|
+
*
|
|
7680
7763
|
* @example
|
|
7681
7764
|
* ```typescript
|
|
7682
7765
|
* const params = new URLSearchParams(window.location.search);
|
|
@@ -7685,6 +7768,7 @@ declare class BrainerceClient {
|
|
|
7685
7768
|
* const { token, customer, isNewCustomer, redirectUrl } =
|
|
7686
7769
|
* await client.exchangeOAuthCode(code);
|
|
7687
7770
|
* client.setCustomerToken(token);
|
|
7771
|
+
* await client.syncCartOnLogin(); // attach the guest cart to the account
|
|
7688
7772
|
* }
|
|
7689
7773
|
* ```
|
|
7690
7774
|
*/
|
|
@@ -8840,6 +8924,11 @@ declare class BrainerceClient {
|
|
|
8840
8924
|
* address text — which is materially less accurate and can place the
|
|
8841
8925
|
* shopper in a neighbouring city's zone, or in none at all.
|
|
8842
8926
|
*
|
|
8927
|
+
* Spreading `getAddressDetails().address` in here is safe: its `lat`, `lng`
|
|
8928
|
+
* and `formattedAddress` are dropped before the request goes out (the
|
|
8929
|
+
* endpoint rejects unknown properties outright, and coordinates are never
|
|
8930
|
+
* taken from the client — the server resolves them from `placeId`).
|
|
8931
|
+
*
|
|
8843
8932
|
* @example
|
|
8844
8933
|
* ```typescript
|
|
8845
8934
|
* const { checkout, rates } = await client.setShippingAddress('checkout_123', {
|
|
@@ -11037,7 +11126,7 @@ declare class BrainerceError extends Error {
|
|
|
11037
11126
|
constructor(message: string, statusCode: number, details?: unknown);
|
|
11038
11127
|
}
|
|
11039
11128
|
|
|
11040
|
-
declare const SDK_VERSION = "1.
|
|
11129
|
+
declare const SDK_VERSION = "1.53.1";
|
|
11041
11130
|
|
|
11042
11131
|
/**
|
|
11043
11132
|
* Verify a webhook signature from Brainerce
|
package/dist/index.js
CHANGED
|
@@ -201,7 +201,7 @@ function isDevGuardsEnabled() {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
// src/version.ts
|
|
204
|
-
var SDK_VERSION = "1.
|
|
204
|
+
var SDK_VERSION = "1.53.1";
|
|
205
205
|
|
|
206
206
|
// src/client.ts
|
|
207
207
|
var DEFAULT_BASE_URL = "https://api.brainerce.com";
|
|
@@ -241,7 +241,7 @@ function parseRetryAfterMs(response) {
|
|
|
241
241
|
function sleep(ms) {
|
|
242
242
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
243
243
|
}
|
|
244
|
-
var
|
|
244
|
+
var _BrainerceClient = class _BrainerceClient {
|
|
245
245
|
constructor(options) {
|
|
246
246
|
this.customerToken = null;
|
|
247
247
|
this.customerCartId = null;
|
|
@@ -260,6 +260,8 @@ var BrainerceClient = class {
|
|
|
260
260
|
// GA4 stitch state (see `loadGoogleAnalytics()` in the Analytics section).
|
|
261
261
|
this._ga4MeasurementId = null;
|
|
262
262
|
this._ga4StitchPromise = null;
|
|
263
|
+
/** One warning per client, not per keystroke-driven address submit. */
|
|
264
|
+
this._warnedResolvedOnlyAddressFields = false;
|
|
263
265
|
/** localStorage key for session cart reference (sessionToken + cartId) */
|
|
264
266
|
this.SESSION_CART_KEY = "brainerce_session";
|
|
265
267
|
/**
|
|
@@ -773,10 +775,19 @@ var BrainerceClient = class {
|
|
|
773
775
|
* Set the customer authentication token (obtained from login/register).
|
|
774
776
|
* Required for accessing customer-specific data in storefront mode.
|
|
775
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
|
+
*
|
|
776
786
|
* @example
|
|
777
787
|
* ```typescript
|
|
778
788
|
* const auth = await client.loginCustomer('user@example.com', 'password');
|
|
779
789
|
* client.setCustomerToken(auth.token);
|
|
790
|
+
* await client.syncCartOnLogin(); // claim the guest cart for this account
|
|
780
791
|
*
|
|
781
792
|
* // Now can access customer data
|
|
782
793
|
* const profile = await client.getMyProfile();
|
|
@@ -1290,6 +1301,39 @@ var BrainerceClient = class {
|
|
|
1290
1301
|
return dto;
|
|
1291
1302
|
}
|
|
1292
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
|
+
}
|
|
1293
1337
|
// -------------------- Products --------------------
|
|
1294
1338
|
/**
|
|
1295
1339
|
* Get a list of products with pagination and filtering
|
|
@@ -2882,6 +2926,12 @@ var BrainerceClient = class {
|
|
|
2882
2926
|
* if (params.get('oauth_success') === 'true' && params.get('auth_code')) {
|
|
2883
2927
|
* const result = await client.exchangeOAuthCode(params.get('auth_code')!);
|
|
2884
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();
|
|
2885
2935
|
* // result.customer, result.isNewCustomer, result.redirectUrl, ...
|
|
2886
2936
|
* } else if (params.get('oauth_error')) {
|
|
2887
2937
|
* // Failures land on this same page, on `redirectUrl` — never on the API
|
|
@@ -2934,6 +2984,11 @@ var BrainerceClient = class {
|
|
|
2934
2984
|
*
|
|
2935
2985
|
* @param authCode - The single-use code from the `?auth_code=` URL param.
|
|
2936
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
|
+
*
|
|
2937
2992
|
* @example
|
|
2938
2993
|
* ```typescript
|
|
2939
2994
|
* const params = new URLSearchParams(window.location.search);
|
|
@@ -2942,6 +2997,7 @@ var BrainerceClient = class {
|
|
|
2942
2997
|
* const { token, customer, isNewCustomer, redirectUrl } =
|
|
2943
2998
|
* await client.exchangeOAuthCode(code);
|
|
2944
2999
|
* client.setCustomerToken(token);
|
|
3000
|
+
* await client.syncCartOnLogin(); // attach the guest cart to the account
|
|
2945
3001
|
* }
|
|
2946
3002
|
* ```
|
|
2947
3003
|
*/
|
|
@@ -5121,6 +5177,11 @@ var BrainerceClient = class {
|
|
|
5121
5177
|
* address text — which is materially less accurate and can place the
|
|
5122
5178
|
* shopper in a neighbouring city's zone, or in none at all.
|
|
5123
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
|
+
*
|
|
5124
5185
|
* @example
|
|
5125
5186
|
* ```typescript
|
|
5126
5187
|
* const { checkout, rates } = await client.setShippingAddress('checkout_123', {
|
|
@@ -5140,7 +5201,9 @@ var BrainerceClient = class {
|
|
|
5140
5201
|
* ```
|
|
5141
5202
|
*/
|
|
5142
5203
|
async setShippingAddress(checkoutId, address) {
|
|
5143
|
-
const body = await this.withAnalyticsStitchIds(
|
|
5204
|
+
const body = await this.withAnalyticsStitchIds(
|
|
5205
|
+
this.stripResolvedOnlyAddressFields(address)
|
|
5206
|
+
);
|
|
5144
5207
|
if (this.isVibeCodedMode()) {
|
|
5145
5208
|
return this.vibeCodedRequest(
|
|
5146
5209
|
"PATCH",
|
|
@@ -5453,24 +5516,25 @@ var BrainerceClient = class {
|
|
|
5453
5516
|
* ```
|
|
5454
5517
|
*/
|
|
5455
5518
|
async setBillingAddress(checkoutId, address) {
|
|
5519
|
+
const body = this.stripResolvedOnlyAddressFields(address);
|
|
5456
5520
|
if (this.isVibeCodedMode()) {
|
|
5457
5521
|
return this.vibeCodedRequest(
|
|
5458
5522
|
"PATCH",
|
|
5459
5523
|
`/checkout/${encodePathSegment(checkoutId)}/billing-address`,
|
|
5460
|
-
|
|
5524
|
+
body
|
|
5461
5525
|
);
|
|
5462
5526
|
}
|
|
5463
5527
|
if (this.storeId && !this.apiKey) {
|
|
5464
5528
|
return this.storefrontRequest(
|
|
5465
5529
|
"PATCH",
|
|
5466
5530
|
`/checkout/${encodePathSegment(checkoutId)}/billing-address`,
|
|
5467
|
-
|
|
5531
|
+
body
|
|
5468
5532
|
);
|
|
5469
5533
|
}
|
|
5470
5534
|
return this.adminRequest(
|
|
5471
5535
|
"PATCH",
|
|
5472
5536
|
`/api/v1/checkout/${encodePathSegment(checkoutId)}/billing-address`,
|
|
5473
|
-
|
|
5537
|
+
body
|
|
5474
5538
|
);
|
|
5475
5539
|
}
|
|
5476
5540
|
/**
|
|
@@ -6436,7 +6500,7 @@ var BrainerceClient = class {
|
|
|
6436
6500
|
const result = await this.vibeCodedRequest(
|
|
6437
6501
|
"PATCH",
|
|
6438
6502
|
`/checkout/${encodePathSegment(checkoutId)}/shipping-address`,
|
|
6439
|
-
data.shippingAddress
|
|
6503
|
+
this.stripResolvedOnlyAddressFields(data.shippingAddress)
|
|
6440
6504
|
);
|
|
6441
6505
|
checkout = result.checkout;
|
|
6442
6506
|
}
|
|
@@ -6444,7 +6508,7 @@ var BrainerceClient = class {
|
|
|
6444
6508
|
checkout = await this.vibeCodedRequest(
|
|
6445
6509
|
"PATCH",
|
|
6446
6510
|
`/checkout/${encodePathSegment(checkoutId)}/billing-address`,
|
|
6447
|
-
data.billingAddress
|
|
6511
|
+
this.stripResolvedOnlyAddressFields(data.billingAddress)
|
|
6448
6512
|
);
|
|
6449
6513
|
}
|
|
6450
6514
|
if (!checkout) {
|
|
@@ -9285,6 +9349,17 @@ var BrainerceClient = class {
|
|
|
9285
9349
|
);
|
|
9286
9350
|
}
|
|
9287
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;
|
|
9288
9363
|
var BrainerceError = class extends Error {
|
|
9289
9364
|
constructor(message, statusCode, details) {
|
|
9290
9365
|
super(message);
|
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.1";
|
|
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;
|
|
@@ -174,6 +174,8 @@ var BrainerceClient = class {
|
|
|
174
174
|
// GA4 stitch state (see `loadGoogleAnalytics()` in the Analytics section).
|
|
175
175
|
this._ga4MeasurementId = null;
|
|
176
176
|
this._ga4StitchPromise = null;
|
|
177
|
+
/** One warning per client, not per keystroke-driven address submit. */
|
|
178
|
+
this._warnedResolvedOnlyAddressFields = false;
|
|
177
179
|
/** localStorage key for session cart reference (sessionToken + cartId) */
|
|
178
180
|
this.SESSION_CART_KEY = "brainerce_session";
|
|
179
181
|
/**
|
|
@@ -687,10 +689,19 @@ var BrainerceClient = class {
|
|
|
687
689
|
* Set the customer authentication token (obtained from login/register).
|
|
688
690
|
* Required for accessing customer-specific data in storefront mode.
|
|
689
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
|
+
*
|
|
690
700
|
* @example
|
|
691
701
|
* ```typescript
|
|
692
702
|
* const auth = await client.loginCustomer('user@example.com', 'password');
|
|
693
703
|
* client.setCustomerToken(auth.token);
|
|
704
|
+
* await client.syncCartOnLogin(); // claim the guest cart for this account
|
|
694
705
|
*
|
|
695
706
|
* // Now can access customer data
|
|
696
707
|
* const profile = await client.getMyProfile();
|
|
@@ -1204,6 +1215,39 @@ var BrainerceClient = class {
|
|
|
1204
1215
|
return dto;
|
|
1205
1216
|
}
|
|
1206
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
|
+
}
|
|
1207
1251
|
// -------------------- Products --------------------
|
|
1208
1252
|
/**
|
|
1209
1253
|
* Get a list of products with pagination and filtering
|
|
@@ -2796,6 +2840,12 @@ var BrainerceClient = class {
|
|
|
2796
2840
|
* if (params.get('oauth_success') === 'true' && params.get('auth_code')) {
|
|
2797
2841
|
* const result = await client.exchangeOAuthCode(params.get('auth_code')!);
|
|
2798
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();
|
|
2799
2849
|
* // result.customer, result.isNewCustomer, result.redirectUrl, ...
|
|
2800
2850
|
* } else if (params.get('oauth_error')) {
|
|
2801
2851
|
* // Failures land on this same page, on `redirectUrl` — never on the API
|
|
@@ -2848,6 +2898,11 @@ var BrainerceClient = class {
|
|
|
2848
2898
|
*
|
|
2849
2899
|
* @param authCode - The single-use code from the `?auth_code=` URL param.
|
|
2850
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
|
+
*
|
|
2851
2906
|
* @example
|
|
2852
2907
|
* ```typescript
|
|
2853
2908
|
* const params = new URLSearchParams(window.location.search);
|
|
@@ -2856,6 +2911,7 @@ var BrainerceClient = class {
|
|
|
2856
2911
|
* const { token, customer, isNewCustomer, redirectUrl } =
|
|
2857
2912
|
* await client.exchangeOAuthCode(code);
|
|
2858
2913
|
* client.setCustomerToken(token);
|
|
2914
|
+
* await client.syncCartOnLogin(); // attach the guest cart to the account
|
|
2859
2915
|
* }
|
|
2860
2916
|
* ```
|
|
2861
2917
|
*/
|
|
@@ -5035,6 +5091,11 @@ var BrainerceClient = class {
|
|
|
5035
5091
|
* address text — which is materially less accurate and can place the
|
|
5036
5092
|
* shopper in a neighbouring city's zone, or in none at all.
|
|
5037
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
|
+
*
|
|
5038
5099
|
* @example
|
|
5039
5100
|
* ```typescript
|
|
5040
5101
|
* const { checkout, rates } = await client.setShippingAddress('checkout_123', {
|
|
@@ -5054,7 +5115,9 @@ var BrainerceClient = class {
|
|
|
5054
5115
|
* ```
|
|
5055
5116
|
*/
|
|
5056
5117
|
async setShippingAddress(checkoutId, address) {
|
|
5057
|
-
const body = await this.withAnalyticsStitchIds(
|
|
5118
|
+
const body = await this.withAnalyticsStitchIds(
|
|
5119
|
+
this.stripResolvedOnlyAddressFields(address)
|
|
5120
|
+
);
|
|
5058
5121
|
if (this.isVibeCodedMode()) {
|
|
5059
5122
|
return this.vibeCodedRequest(
|
|
5060
5123
|
"PATCH",
|
|
@@ -5367,24 +5430,25 @@ var BrainerceClient = class {
|
|
|
5367
5430
|
* ```
|
|
5368
5431
|
*/
|
|
5369
5432
|
async setBillingAddress(checkoutId, address) {
|
|
5433
|
+
const body = this.stripResolvedOnlyAddressFields(address);
|
|
5370
5434
|
if (this.isVibeCodedMode()) {
|
|
5371
5435
|
return this.vibeCodedRequest(
|
|
5372
5436
|
"PATCH",
|
|
5373
5437
|
`/checkout/${encodePathSegment(checkoutId)}/billing-address`,
|
|
5374
|
-
|
|
5438
|
+
body
|
|
5375
5439
|
);
|
|
5376
5440
|
}
|
|
5377
5441
|
if (this.storeId && !this.apiKey) {
|
|
5378
5442
|
return this.storefrontRequest(
|
|
5379
5443
|
"PATCH",
|
|
5380
5444
|
`/checkout/${encodePathSegment(checkoutId)}/billing-address`,
|
|
5381
|
-
|
|
5445
|
+
body
|
|
5382
5446
|
);
|
|
5383
5447
|
}
|
|
5384
5448
|
return this.adminRequest(
|
|
5385
5449
|
"PATCH",
|
|
5386
5450
|
`/api/v1/checkout/${encodePathSegment(checkoutId)}/billing-address`,
|
|
5387
|
-
|
|
5451
|
+
body
|
|
5388
5452
|
);
|
|
5389
5453
|
}
|
|
5390
5454
|
/**
|
|
@@ -6350,7 +6414,7 @@ var BrainerceClient = class {
|
|
|
6350
6414
|
const result = await this.vibeCodedRequest(
|
|
6351
6415
|
"PATCH",
|
|
6352
6416
|
`/checkout/${encodePathSegment(checkoutId)}/shipping-address`,
|
|
6353
|
-
data.shippingAddress
|
|
6417
|
+
this.stripResolvedOnlyAddressFields(data.shippingAddress)
|
|
6354
6418
|
);
|
|
6355
6419
|
checkout = result.checkout;
|
|
6356
6420
|
}
|
|
@@ -6358,7 +6422,7 @@ var BrainerceClient = class {
|
|
|
6358
6422
|
checkout = await this.vibeCodedRequest(
|
|
6359
6423
|
"PATCH",
|
|
6360
6424
|
`/checkout/${encodePathSegment(checkoutId)}/billing-address`,
|
|
6361
|
-
data.billingAddress
|
|
6425
|
+
this.stripResolvedOnlyAddressFields(data.billingAddress)
|
|
6362
6426
|
);
|
|
6363
6427
|
}
|
|
6364
6428
|
if (!checkout) {
|
|
@@ -9199,6 +9263,17 @@ var BrainerceClient = class {
|
|
|
9199
9263
|
);
|
|
9200
9264
|
}
|
|
9201
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;
|
|
9202
9277
|
var BrainerceError = class extends Error {
|
|
9203
9278
|
constructor(message, statusCode, details) {
|
|
9204
9279
|
super(message);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainerce",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.53.1",
|
|
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",
|