brainerce 1.58.1 → 1.59.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 +299 -102
- package/dist/index.d.mts +154 -28
- package/dist/index.d.ts +154 -28
- package/dist/index.js +38 -18
- package/dist/index.mjs +38 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,14 +7,31 @@ This SDK provides a complete solution for vibe-coded sites, AI-built stores (Cur
|
|
|
7
7
|
> **AI Agents / Vibe Coders (Cursor, Lovable, Claude Code, VS Code):** Use the MCP server for AI-powered store building: `npx @brainerce/mcp-server`. It provides docs, code templates, and live store capabilities directly inside your IDE.
|
|
8
8
|
> Note: the MCP server runs inside your IDE — it is not available in chat-only tools like Google AI Studio or ChatGPT.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Three SDK modes — choose the right one
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
|
15
|
-
|
|
|
12
|
+
The exported class is **`BrainerceClient`**. Which mode you get is decided by which key you pass to its constructor:
|
|
13
|
+
|
|
14
|
+
| Mode | Config key | Use for | Where to run |
|
|
15
|
+
| ----------------- | ------------------------ | ---------------------------------------- | ----------------------------------- |
|
|
16
|
+
| **Sales channel** | `salesChannelId: 'vc_*'` | Building the customer-facing store | Browser / client-side |
|
|
17
|
+
| **Storefront** | `storeId` | A public storefront on a published store | Browser / client-side |
|
|
18
|
+
| **Admin** | `apiKey: 'brainerce_*'` | Managing products, team, settings | Server only — never in browser code |
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { BrainerceClient } from 'brainerce';
|
|
22
|
+
|
|
23
|
+
const client = new BrainerceClient({ salesChannelId: 'vc_abc123' });
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**If you pass more than one, `apiKey` wins, then `salesChannelId`, then `storeId`.** An `apiKey` puts the client in admin mode no matter what else you passed, so never add one "just to also read a channel" — it changes every route the client calls. Passing none throws `BrainerceClient: either salesChannelId, apiKey, or storeId is required`.
|
|
27
|
+
|
|
28
|
+
Ask the client which mode it is in with `isSalesChannelMode()`, `isStorefrontMode()` or `isAdminMode()` — exactly one returns `true`. (`isVibeCodedMode()` is a deprecated alias of `isSalesChannelMode()`.)
|
|
29
|
+
|
|
30
|
+
Not every method works in every mode. A handful — `getPaymentStatus()`, `confirmSdkPayment()`, `waitForOrder()` — are **sales-channel mode only** and throw `BrainerceError` 400 elsewhere. `storeId` mode is **not** read-only: it can create carts, run a checkout to a real order, and register/log in customers; what it cannot reach is the admin surface.
|
|
16
31
|
|
|
17
32
|
> **Building a storefront?** You only need your **Sales Channel ID** (`vc_*`) from the Brainerce dashboard under **Sales Channels**. No API key needed. API keys are a server-side admin secret.
|
|
33
|
+
>
|
|
34
|
+
> `connectionId` is the deprecated alias of `salesChannelId`. It still works, logs a deprecation warning on every construction, and is removed in SDK 2.0.
|
|
18
35
|
|
|
19
36
|
## Installation
|
|
20
37
|
|
|
@@ -68,7 +85,7 @@ Violating any of these causes production incidents or broken orders. Read them b
|
|
|
68
85
|
- ALWAYS call SDK client methods. Never reconstruct REST URLs or call `fetch` directly.
|
|
69
86
|
- NEVER invent SDK method names. If it's not in this README or in `get-sdk-docs`, it doesn't exist.
|
|
70
87
|
- NEVER hardcode product data, categories, or store copy — Brainerce is the database.
|
|
71
|
-
- NEVER use `submitGuestOrder()` or `createOrder()` — they
|
|
88
|
+
- NEVER use `submitGuestOrder()`, `createGuestOrder()` or `createOrder()` on a store that takes payment — they `POST /orders` directly, never touch `/payment/intent`, and produce an order nobody has paid for. They exist only for cash-on-delivery, manual-invoice and sandbox stores. Everything else goes through the checkout sequence below.
|
|
72
89
|
- ALWAYS use SDK helpers (`getCartTotals`, `formatPrice`, `getProductPriceInfo`, `getCartItemImage`, `getCartItemName`, `getVariantPrice`, `getStockStatus`, `getDescriptionContent`) instead of reading raw fields.
|
|
73
90
|
|
|
74
91
|
### State management
|
|
@@ -419,7 +436,7 @@ await client.addToCart(cart.id, {
|
|
|
419
436
|
});
|
|
420
437
|
```
|
|
421
438
|
|
|
422
|
-
Money on the wire is **always strings** (`priceDelta: "5.00"`). Validation failures arrive as a structured 400 envelope on `BrainerceError.details` with `code: 'MODIFIER_VALIDATION_FAILED'
|
|
439
|
+
Money on the wire is **always strings** (`priceDelta: "5.00"`). Validation failures arrive as a structured 400 envelope on `BrainerceError.details` with `code: 'MODIFIER_VALIDATION_FAILED'`; the per-issue list is nested at `details.errors[]`, so from the SDK it reads `err.details.details.errors` (`err.details` is the whole response body) — see INTEGRATION-RULES.md "Modifier validation errors" for the full code list.
|
|
423
440
|
|
|
424
441
|
Full rendering guide: [Core Integration §2.9](https://brainerce.com/docs/integration/core). Restaurant features (scheduled availability, nested combos to depth 3, downsell modifiers): [Optional Features "Restaurant / build-your-own products"](https://brainerce.com/docs/integration/optional).
|
|
425
442
|
|
|
@@ -573,14 +590,16 @@ if (result.tracked) {
|
|
|
573
590
|
// Continue with payment flow...
|
|
574
591
|
}
|
|
575
592
|
|
|
576
|
-
//
|
|
593
|
+
// ⚠️ CASH-ON-DELIVERY / SANDBOX ONLY - submitGuestOrder() places the order with
|
|
594
|
+
// NO payment collected. Never call it on a store with a payment provider.
|
|
577
595
|
const order = await client.submitGuestOrder();
|
|
578
596
|
```
|
|
579
597
|
|
|
580
598
|
**Rule of thumb:**
|
|
581
599
|
|
|
582
|
-
- Guest user + Session cart → `startGuestCheckout()`
|
|
600
|
+
- Guest user + Session cart → `startGuestCheckout()`
|
|
583
601
|
- Logged-in user + Server cart → `createCheckout({ cartId })`
|
|
602
|
+
- Store that collects no money at checkout (cash on delivery, manual invoice, sandbox) → `submitGuestOrder()`
|
|
584
603
|
|
|
585
604
|
### 2. ⛔ NEVER Create Local Interfaces - Use SDK Types!
|
|
586
605
|
|
|
@@ -1072,6 +1091,9 @@ if (!address.country) {
|
|
|
1072
1091
|
// guaranteed) the same ISO 3166-2 subdivision code this store's own region
|
|
1073
1092
|
// lists use. Validate against destinations.regions before trusting it;
|
|
1074
1093
|
// never assign a code the region <select> wouldn't recognize.
|
|
1094
|
+
// NOTE: getShippingDestinations() is CHANNEL-ONLY — it needs a `salesChannelId`
|
|
1095
|
+
// client. In `storeId` (public storefront) mode it targets a path the API does not
|
|
1096
|
+
// expose and returns 404, so drive the region <select> from your own list there.
|
|
1075
1097
|
const destinations = await client.getShippingDestinations();
|
|
1076
1098
|
const validRegions = destinations.regions[address.country] ?? [];
|
|
1077
1099
|
const region = validRegions.some((r) => r.code === address.region) ? address.region : '';
|
|
@@ -1666,27 +1688,58 @@ function SearchInput() {
|
|
|
1666
1688
|
|
|
1667
1689
|
#### Product Type Definition
|
|
1668
1690
|
|
|
1691
|
+
> **The shipped `.d.ts` is the authority.** These are abridged for reading —
|
|
1692
|
+
> import the real types (`import type { Product, ProductVariant } from 'brainerce'`)
|
|
1693
|
+
> rather than retyping them. See the Critical Rule: _never write your own copies
|
|
1694
|
+
> of SDK types._
|
|
1695
|
+
|
|
1669
1696
|
```typescript
|
|
1670
1697
|
interface Product {
|
|
1671
1698
|
id: string;
|
|
1672
1699
|
name: string;
|
|
1673
|
-
|
|
1674
|
-
|
|
1700
|
+
slug?: string | null; // link target for /products/{slug}
|
|
1701
|
+
localeSlugs?: Record<string, string> | null; // per-locale slugs for hreflang / sitemaps
|
|
1702
|
+
description?: string | null; // HTML — sanitize before rendering (may contain <video>/<iframe>)
|
|
1703
|
+
descriptionFormat?: 'text' | 'html' | 'markdown' | null;
|
|
1675
1704
|
sku: string;
|
|
1705
|
+
gtin?: string | null; // EAN/UPC/ISBN — emitted in Product JSON-LD
|
|
1706
|
+
mpn?: string | null; // Manufacturer Part Number — emitted in Product JSON-LD
|
|
1707
|
+
faq?: Array<{ q: string; a: string }> | null; // render as a FAQ section + buildProductFaqJsonLd()
|
|
1676
1708
|
basePrice: string; // Decimal as string — use parseFloat() for calculations
|
|
1677
1709
|
salePrice?: string | null;
|
|
1678
1710
|
salePriceStartsAt?: string | null; // ISO 8601 — sale-price effective window start
|
|
1679
1711
|
salePriceEndsAt?: string | null; // ISO 8601 — sale-price window end; feeds buildProductJsonLd's priceValidUntil
|
|
1712
|
+
costPrice?: string | null; // COGS — admin (apiKey) reads only
|
|
1680
1713
|
priceMin?: string | null; // Lowest variant price (VARIABLE products only)
|
|
1681
1714
|
priceMax?: string | null; // Highest variant price (VARIABLE products only)
|
|
1682
1715
|
priceVaries?: boolean; // true when range should be shown ("₪49 – ₪199")
|
|
1716
|
+
// Region FX overlay — present ONLY with getProducts({ regionId }). DISPLAY-ONLY;
|
|
1717
|
+
// basePrice/salePrice stay in the store currency. Use formatProductPrice().
|
|
1718
|
+
displayPrice?: string;
|
|
1719
|
+
displaySalePrice?: string;
|
|
1720
|
+
displayPriceMin?: string;
|
|
1721
|
+
displayPriceMax?: string;
|
|
1722
|
+
displayCurrency?: string; // ISO 4217 of the display* fields
|
|
1683
1723
|
status: string; // e.g. "active" | "draft"
|
|
1684
1724
|
type: 'SIMPLE' | 'VARIABLE';
|
|
1725
|
+
isDownloadable?: boolean;
|
|
1726
|
+
downloads?: DownloadFile[] | null; // when isDownloadable
|
|
1685
1727
|
images?: ProductImage[];
|
|
1686
1728
|
inventory?: InventoryInfo | null;
|
|
1687
1729
|
variants?: ProductVariant[];
|
|
1688
1730
|
categories?: Array<{ id: string; name: string; slug?: string | null }>; // NOT string[] — use slug to link to /category/{slug}
|
|
1731
|
+
brands?: Array<{ id: string; name: string }>; // objects, not string[]
|
|
1689
1732
|
tags?: string[];
|
|
1733
|
+
metafields?: ProductMetafield[]; // custom fields — check field.type before rendering
|
|
1734
|
+
customizationFields?: ProductCustomizationField[]; // buyer input (engraving, uploads…)
|
|
1735
|
+
modifierGroups?: ModifierGroup[]; // add-ons / options priced per selection
|
|
1736
|
+
discount?: ProductDiscount | null; // active rule-based discount on this product
|
|
1737
|
+
avgRating?: number; // feeds JSON-LD aggregateRating
|
|
1738
|
+
reviewCount?: number;
|
|
1739
|
+
needsSync: boolean; // REQUIRED — always returned
|
|
1740
|
+
taxBehavior?: 'taxable' | 'exempt';
|
|
1741
|
+
menuOrder?: number | null;
|
|
1742
|
+
channelPublishes?: Array<{ salesChannel: { id: string; name: string; connectionId: string } }>; // admin mode only
|
|
1690
1743
|
createdAt: string;
|
|
1691
1744
|
updatedAt: string;
|
|
1692
1745
|
}
|
|
@@ -1695,28 +1748,48 @@ interface ProductImage {
|
|
|
1695
1748
|
url: string;
|
|
1696
1749
|
position?: number;
|
|
1697
1750
|
isMain?: boolean;
|
|
1751
|
+
alt?: string;
|
|
1752
|
+
// admin (apiKey) mode only: id, key, thumbnailUrl, width, height, size, mimeType, createdAt
|
|
1698
1753
|
}
|
|
1699
1754
|
|
|
1700
1755
|
interface ProductVariant {
|
|
1701
1756
|
id: string;
|
|
1757
|
+
productId: string; // REQUIRED — parent product
|
|
1702
1758
|
sku?: string | null;
|
|
1703
1759
|
name?: string | null;
|
|
1704
|
-
price?:
|
|
1705
|
-
salePrice?:
|
|
1706
|
-
|
|
1760
|
+
price?: string | null; // STRING, not number — parseFloat() before any math
|
|
1761
|
+
salePrice?: string | null; // STRING, not number
|
|
1762
|
+
costPrice?: string | null; // COGS override — admin (apiKey) reads only
|
|
1763
|
+
// Region FX overlay — present ONLY with getProducts({ regionId }). Display-only.
|
|
1764
|
+
displayPrice?: string;
|
|
1765
|
+
displaySalePrice?: string;
|
|
1766
|
+
displayCurrency?: string;
|
|
1767
|
+
attributes?: Record<string, string> | null; // e.g. { "Color": "Red", "Size": "M" }
|
|
1768
|
+
options?: Array<{ name: string; value: string }>; // alternative shape — use getVariantOptions()
|
|
1707
1769
|
inventory?: InventoryInfo | null;
|
|
1770
|
+
image?: string | { url: string; thumbnailUrl?: string } | null;
|
|
1771
|
+
position: number; // REQUIRED — display order
|
|
1772
|
+
status?: string | null;
|
|
1773
|
+
createdAt: string; // REQUIRED
|
|
1774
|
+
updatedAt: string; // REQUIRED
|
|
1708
1775
|
}
|
|
1709
1776
|
|
|
1710
1777
|
interface InventoryInfo {
|
|
1711
1778
|
total: number;
|
|
1712
1779
|
reserved: number;
|
|
1713
1780
|
available: number;
|
|
1714
|
-
trackingMode
|
|
1781
|
+
trackingMode: InventoryTrackingMode; // REQUIRED — 'TRACKED' | 'UNLIMITED' | 'DISABLED'
|
|
1715
1782
|
inStock: boolean; // Pre-calculated - use this for display!
|
|
1716
1783
|
canPurchase: boolean; // Pre-calculated - use this for add-to-cart
|
|
1784
|
+
lastInventorySyncAt?: string | null; // admin mode only
|
|
1717
1785
|
}
|
|
1718
1786
|
```
|
|
1719
1787
|
|
|
1788
|
+
> **Variant prices are strings.** `variant.price` and `variant.salePrice` are
|
|
1789
|
+
> `string | null`, exactly like `product.basePrice`. `variant.price > 100` compares
|
|
1790
|
+
> lexicographically and silently returns the wrong answer — always `parseFloat()`
|
|
1791
|
+
> first, or use `getVariantPrice(variant)` / `formatVariantPrice(variant)`.
|
|
1792
|
+
|
|
1720
1793
|
#### Product Metafields (Custom Fields)
|
|
1721
1794
|
|
|
1722
1795
|
Products can have custom fields (metafields) defined by the store owner, such as "Material", "Care Instructions", or "Warranty".
|
|
@@ -1907,15 +1980,20 @@ await client.addToCart(cartId, {
|
|
|
1907
1980
|
| GALLERY | `string[]` (URLs) | Multi-file upload |
|
|
1908
1981
|
| DIMENSION/WEIGHT | `{ value, unit }` | Value + unit inputs |
|
|
1909
1982
|
|
|
1910
|
-
**
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1983
|
+
**Assigning fields to a product is dashboard-only — there is no SDK path to it.**
|
|
1984
|
+
`client.setProductCustomizationFields()` and `client.getProductCustomizationFields()`
|
|
1985
|
+
target `/api/v1/metafield-definitions/products/:productId/customization-fields`, which
|
|
1986
|
+
the public API does not expose; both return **404**. Those routes exist only on the
|
|
1987
|
+
dashboard API (`/api/stores/:storeId/metafield-definitions/products/:productId/customization-fields`),
|
|
1988
|
+
behind Clerk auth. Choose which customer-input definitions apply to a product in the
|
|
1989
|
+
dashboard.
|
|
1990
|
+
|
|
1991
|
+
> **Two different things share the name `getProductCustomizationFields`.** The
|
|
1992
|
+
> **exported helper** used above — `import { getProductCustomizationFields } from 'brainerce'`
|
|
1993
|
+
> — is a pure function that reads the definitions off a product you already fetched. It
|
|
1994
|
+
> works in every mode and is the one you want. The **client method** of the same name,
|
|
1995
|
+
> which writes the assignment, is the one that 404s. Reading is fully covered without it:
|
|
1996
|
+
> `product.customizationFields` is already on every product response.
|
|
1919
1997
|
|
|
1920
1998
|
> **Note:** `customizationFields` is only present when the product has customer input fields assigned. After checkout, customization values are preserved in the order as `item.customizations`.
|
|
1921
1999
|
|
|
@@ -2117,9 +2195,17 @@ const totals = getCartTotals(cart);
|
|
|
2117
2195
|
|
|
2118
2196
|
---
|
|
2119
2197
|
|
|
2120
|
-
### Guest Checkout (Submit Order)
|
|
2198
|
+
### Guest Checkout (Submit Order) — no payment collected
|
|
2121
2199
|
|
|
2122
|
-
>
|
|
2200
|
+
> **⛔ Not for stores that take payment.** `submitGuestOrder()` posts the order
|
|
2201
|
+
> straight to `POST /orders`. It never creates a payment intent, so the order is
|
|
2202
|
+
> created **unpaid** and no card is ever charged. Use it only where checkout
|
|
2203
|
+
> collects no money — cash on delivery, manual invoicing, or a sandbox store.
|
|
2204
|
+
>
|
|
2205
|
+
> For every other store use `startGuestCheckout()`, which creates a real checkout
|
|
2206
|
+
> session from the session cart and hands you a `checkoutId` to run the payment
|
|
2207
|
+
> flow against. This is the same rule as the Critical Rule above; the two used to
|
|
2208
|
+
> disagree.
|
|
2123
2209
|
|
|
2124
2210
|
```typescript
|
|
2125
2211
|
// Make sure cart has items, customer email, and shipping address
|
|
@@ -2227,7 +2313,8 @@ if (checkout.tracked) {
|
|
|
2227
2313
|
const order = await client.completeGuestCheckout(checkout.checkoutId);
|
|
2228
2314
|
console.log('Order created:', order.orderId);
|
|
2229
2315
|
} else {
|
|
2230
|
-
// Fallback
|
|
2316
|
+
// Fallback — NO payment is collected on this path. Only reachable on a
|
|
2317
|
+
// cash-on-delivery / manual-invoice / sandbox store.
|
|
2231
2318
|
const order = await client.submitGuestOrder();
|
|
2232
2319
|
}
|
|
2233
2320
|
```
|
|
@@ -2426,8 +2513,11 @@ await client.getProduct('prod_tshirt', { regionId: 'region_eu' });
|
|
|
2426
2513
|
await client.getProductBySlug('blue-shirt', { regionId: 'region_eu' });
|
|
2427
2514
|
```
|
|
2428
2515
|
|
|
2429
|
-
> **Display-only
|
|
2430
|
-
>
|
|
2516
|
+
> **Display-only, and unlike checkout.** `regionId` on a _product read_ never
|
|
2517
|
+
> affects what is charged — it only changes what you render. `regionId` on
|
|
2518
|
+
> `createCheckout()` is different: it CAN charge the region currency (see
|
|
2519
|
+
> FX-at-checkout above). Do not carry the "display-only" assumption from here into
|
|
2520
|
+
> the checkout step. Works in
|
|
2431
2521
|
> **all three modes** — vibe-coded (`vc_*`/`salesChannelId`), storefront (`storeId`),
|
|
2432
2522
|
> and admin (`apiKey`). The response gains `displayPrice` whenever a daily FX rate
|
|
2433
2523
|
> exists for the store/region currency pair in **either** direction (the overlay
|
|
@@ -2917,16 +3007,21 @@ if (paypalProvider) {
|
|
|
2917
3007
|
}
|
|
2918
3008
|
```
|
|
2919
3009
|
|
|
2920
|
-
#### Get Payment Configuration (Single Provider)
|
|
3010
|
+
#### Get Payment Configuration (Single Provider) — DEPRECATED
|
|
2921
3011
|
|
|
2922
|
-
|
|
3012
|
+
> **`getPaymentConfig()` is `@deprecated`.** It only ever describes one provider, so
|
|
3013
|
+
> a store with an additive express method (PayPal, a wallet) renders wrong. Use
|
|
3014
|
+
> `getPaymentProviders()` above. This section documents the legacy shape for code
|
|
3015
|
+
> that still calls it.
|
|
2923
3016
|
|
|
2924
3017
|
```typescript
|
|
2925
3018
|
const config = await client.getPaymentConfig();
|
|
2926
3019
|
|
|
2927
3020
|
// Returns:
|
|
2928
3021
|
// {
|
|
2929
|
-
// provider: 'stripe' | 'paypal'
|
|
3022
|
+
// provider: string, // 'stripe' | 'paypal' | 'grow' | 'cardcom' | any
|
|
3023
|
+
// // installed marketplace payment app — NOT a
|
|
3024
|
+
// // closed union. Never switch on it exhaustively.
|
|
2930
3025
|
// publicKey: 'pk_live_xxx...', // Stripe publishable key or PayPal client ID
|
|
2931
3026
|
// supportedMethods: ['card', 'ideal', 'bancontact'],
|
|
2932
3027
|
// testMode: false
|
|
@@ -2943,13 +3038,74 @@ const intent = await client.createPaymentIntent(checkout.id);
|
|
|
2943
3038
|
// Returns:
|
|
2944
3039
|
// {
|
|
2945
3040
|
// id: 'pi_xxx...',
|
|
2946
|
-
// clientSecret: 'pi_xxx_secret_xxx', //
|
|
2947
|
-
// amount:
|
|
3041
|
+
// clientSecret: 'pi_xxx_secret_xxx', // See the note below — NOT always a secret
|
|
3042
|
+
// amount: '99.99', // DECIMAL STRING, not cents. parseFloat() it.
|
|
2948
3043
|
// currency: 'USD',
|
|
2949
|
-
// status: 'requires_payment_method'
|
|
3044
|
+
// status: 'requires_payment_method',
|
|
3045
|
+
// provider: 'stripe', // which processor took the intent
|
|
3046
|
+
// clientSdk: { renderType: 'sdk-widget', /* … */ }, // HOW to render — see below
|
|
2950
3047
|
// }
|
|
2951
3048
|
```
|
|
2952
3049
|
|
|
3050
|
+
> **`amount` is a decimal string in the charged currency (`"99.99"`), never an
|
|
3051
|
+
> integer of cents.** `parseFloat()` it before any math; do not divide by 100.
|
|
3052
|
+
|
|
3053
|
+
**`provider` and `clientSdk` are the two fields that decide what you render**, and
|
|
3054
|
+
both are omitted from most copy-paste snippets. `clientSdk.renderType` is one of
|
|
3055
|
+
`'sdk-widget' | 'iframe' | 'redirect' | 'sandbox' | 'embedded-fields'`:
|
|
3056
|
+
|
|
3057
|
+
| `renderType` | What `clientSecret` holds | What to do |
|
|
3058
|
+
| -------------- | ------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
3059
|
+
| `'sandbox'` | (unused) | No payment UI — complete the checkout directly |
|
|
3060
|
+
| `'sdk-widget'` | Client secret / auth code | Load `clientSdk.scriptUrl`, init with `clientSdk.initConfig`, mount into `clientSdk.containerId` |
|
|
3061
|
+
| `'iframe'` | **A URL** | Load it in an iframe (inline if its path contains `/embed/`, else in a modal) |
|
|
3062
|
+
| `'redirect'` | **A URL** | Navigate the top-level window to it; on return call `confirmSdkPayment()` |
|
|
3063
|
+
|
|
3064
|
+
Branch on `clientSdk?.renderType`. **Never** branch on "does `clientSdk` exist" —
|
|
3065
|
+
every provider returns one, sandbox included — and never hard-code by provider name.
|
|
3066
|
+
Only `provider === 'stripe'` has a `clientSdk.initConfig.publishableKey`.
|
|
3067
|
+
|
|
3068
|
+
#### Confirm an SDK / redirect payment
|
|
3069
|
+
|
|
3070
|
+
```typescript
|
|
3071
|
+
// confirmSdkPayment(checkoutId, providerResponseData?) => Promise<{ confirmed: boolean }>
|
|
3072
|
+
await client.confirmSdkPayment(checkoutId);
|
|
3073
|
+
|
|
3074
|
+
// With the provider's own callback payload (transactionId, transactionToken,
|
|
3075
|
+
// confirmation_number, …) when its SDK hands you one:
|
|
3076
|
+
await client.confirmSdkPayment(checkoutId, { transactionId: 'txn_123' });
|
|
3077
|
+
```
|
|
3078
|
+
|
|
3079
|
+
Call it in two places:
|
|
3080
|
+
|
|
3081
|
+
- **In an in-page SDK's success callback** (`renderType: 'sdk-widget'`) — it tells
|
|
3082
|
+
the backend the payment succeeded, which triggers order creation.
|
|
3083
|
+
- **On the return page from a `renderType: 'redirect'` provider** — redirect
|
|
3084
|
+
providers don't capture until the server confirms, so this is what makes the
|
|
3085
|
+
backend verify with the provider and capture.
|
|
3086
|
+
|
|
3087
|
+
It is **idempotent** (safe if a webhook already captured) and safe to skip on
|
|
3088
|
+
failure — `getPaymentStatus()` / `waitForOrder()` re-verify server-side. Wrap it in
|
|
3089
|
+
`try/catch` and carry on:
|
|
3090
|
+
|
|
3091
|
+
```typescript
|
|
3092
|
+
try {
|
|
3093
|
+
await client.confirmSdkPayment(checkoutId);
|
|
3094
|
+
} catch {
|
|
3095
|
+
// Not fatal — polling re-verifies.
|
|
3096
|
+
}
|
|
3097
|
+
const result = await client.waitForOrder(checkoutId);
|
|
3098
|
+
```
|
|
3099
|
+
|
|
3100
|
+
Do **not** call it on your `cancelUrl` — the buyer abandoned; just let them retry.
|
|
3101
|
+
|
|
3102
|
+
`confirmGrowPayment()` is a deprecated wrapper around this method; call
|
|
3103
|
+
`confirmSdkPayment()` directly.
|
|
3104
|
+
|
|
3105
|
+
> `confirmSdkPayment()`, `getPaymentStatus()`, `createPaymentIntent()`,
|
|
3106
|
+
> `getPaymentProviders()` and `waitForOrder()` are **sales-channel mode only** —
|
|
3107
|
+
> they throw `BrainerceError` 400 on a `storeId` or `apiKey` client.
|
|
3108
|
+
|
|
2953
3109
|
**Routing to a specific provider (`providerId`).** With `getPaymentProviders()` you
|
|
2954
3110
|
render additive **express buttons** (e.g. PayPal as a `WALLET`) alongside the primary
|
|
2955
3111
|
card form. When the buyer taps one, pass that provider's `id` so the charge routes to
|
|
@@ -2974,7 +3130,10 @@ selectable.
|
|
|
2974
3130
|
Use the client secret with Stripe.js to collect payment:
|
|
2975
3131
|
|
|
2976
3132
|
```typescript
|
|
2977
|
-
// Initialize Stripe.js with the
|
|
3133
|
+
// Initialize Stripe.js with the publishable key. Prefer
|
|
3134
|
+
// intent.clientSdk.initConfig.publishableKey (per-intent, correct for the
|
|
3135
|
+
// provider that actually took the charge); config.publicKey is the deprecated
|
|
3136
|
+
// getPaymentConfig() fallback.
|
|
2978
3137
|
const stripe = await loadStripe(config.publicKey);
|
|
2979
3138
|
|
|
2980
3139
|
// Create Elements and Payment Element
|
|
@@ -3131,7 +3290,10 @@ export default function CheckoutPaymentPage() {
|
|
|
3131
3290
|
const checkoutId = new URLSearchParams(window.location.search).get('checkout_id');
|
|
3132
3291
|
if (!checkoutId) throw new Error('No checkout ID');
|
|
3133
3292
|
|
|
3134
|
-
// Get payment configuration
|
|
3293
|
+
// Get payment configuration.
|
|
3294
|
+
// NOTE: getPaymentConfig() is @deprecated — single-provider only. In new
|
|
3295
|
+
// code use getPaymentProviders() and render additive express methods
|
|
3296
|
+
// alongside the primary card form. Kept here as a minimal Stripe example.
|
|
3135
3297
|
const config = await client.getPaymentConfig();
|
|
3136
3298
|
setPaymentConfig(config);
|
|
3137
3299
|
|
|
@@ -3306,10 +3468,23 @@ if (intent.clientSdk?.renderType === 'sandbox') {
|
|
|
3306
3468
|
|
|
3307
3469
|
#### Register Customer
|
|
3308
3470
|
|
|
3471
|
+
> **Password policy — enforced on `registerCustomer()` AND `resetPassword()`.**
|
|
3472
|
+
> At least **8 characters**, with at least one **lowercase** letter, one
|
|
3473
|
+
> **uppercase** letter, one **digit**, and one **special character**
|
|
3474
|
+
> (`/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z0-9]).{8,}$/`). A password that
|
|
3475
|
+
> fails returns HTTP 400 with the message _"Password must contain at least 1
|
|
3476
|
+
> uppercase letter, 1 lowercase letter, 1 number, and 1 special character"_.
|
|
3477
|
+
>
|
|
3478
|
+
> `securepassword123` fails (no uppercase, no special). `Password123` fails (no
|
|
3479
|
+
> special). `SecurePass123!` passes. Mirror the full rule in your own client-side
|
|
3480
|
+
> validation and in the field's helper text — a form that only says "min 8
|
|
3481
|
+
> characters" produces a 400 the shopper cannot explain, and render the server's
|
|
3482
|
+
> message verbatim when one comes back.
|
|
3483
|
+
|
|
3309
3484
|
```typescript
|
|
3310
3485
|
const auth = await client.registerCustomer({
|
|
3311
3486
|
email: 'customer@example.com',
|
|
3312
|
-
password: '
|
|
3487
|
+
password: 'SecurePass123!',
|
|
3313
3488
|
firstName: 'John',
|
|
3314
3489
|
lastName: 'Doe',
|
|
3315
3490
|
});
|
|
@@ -3319,7 +3494,8 @@ if (auth.requiresVerification) {
|
|
|
3319
3494
|
localStorage.setItem('verificationToken', auth.token);
|
|
3320
3495
|
window.location.href = '/verify-email';
|
|
3321
3496
|
} else {
|
|
3322
|
-
setCustomerToken(auth.token);
|
|
3497
|
+
client.setCustomerToken(auth.token);
|
|
3498
|
+
await client.syncCartOnLogin(); // REQUIRED — claims the guest cart onto the account
|
|
3323
3499
|
// Redirect back to store, not /account
|
|
3324
3500
|
window.location.href = '/';
|
|
3325
3501
|
}
|
|
@@ -3328,8 +3504,9 @@ if (auth.requiresVerification) {
|
|
|
3328
3504
|
#### Login Customer
|
|
3329
3505
|
|
|
3330
3506
|
```typescript
|
|
3331
|
-
const auth = await client.loginCustomer('customer@example.com', '
|
|
3332
|
-
setCustomerToken(auth.token);
|
|
3507
|
+
const auth = await client.loginCustomer('customer@example.com', 'SecurePass123!');
|
|
3508
|
+
client.setCustomerToken(auth.token);
|
|
3509
|
+
await client.syncCartOnLogin(); // REQUIRED — claims the guest cart onto the account
|
|
3333
3510
|
|
|
3334
3511
|
// Best practice: redirect back to previous page or home
|
|
3335
3512
|
const returnUrl = localStorage.getItem('returnUrl') || '/';
|
|
@@ -3337,6 +3514,13 @@ localStorage.removeItem('returnUrl');
|
|
|
3337
3514
|
window.location.href = returnUrl;
|
|
3338
3515
|
```
|
|
3339
3516
|
|
|
3517
|
+
> **`setCustomerToken()` is a plain field setter — it does not touch the cart.**
|
|
3518
|
+
> Always follow it with `await client.syncCartOnLogin()`. Skip it and the
|
|
3519
|
+
> shopper's guest cart is never attached to their account, which quietly breaks
|
|
3520
|
+
> every identity-keyed feature: first-order discounts, per-customer coupon caps,
|
|
3521
|
+
> and abandoned-cart recovery. Same rule after `verifyEmail()` and after
|
|
3522
|
+
> `exchangeOAuthCode()`.
|
|
3523
|
+
|
|
3340
3524
|
> **Best Practice:** Before showing login page, save the current URL with `localStorage.setItem('returnUrl', window.location.pathname)`. After login, redirect back to that URL. This is how Amazon, Shopify, and most e-commerce sites work.
|
|
3341
3525
|
|
|
3342
3526
|
#### Forgot Password
|
|
@@ -3352,7 +3536,7 @@ await client.forgotPassword('customer@example.com');
|
|
|
3352
3536
|
```typescript
|
|
3353
3537
|
// On /reset-password page, extract token from URL
|
|
3354
3538
|
const token = new URLSearchParams(window.location.search).get('token');
|
|
3355
|
-
const result = await client.resetPassword(token!, '
|
|
3539
|
+
const result = await client.resetPassword(token!, 'NewSecurePass123!');
|
|
3356
3540
|
// result.message = "Password has been reset successfully"
|
|
3357
3541
|
```
|
|
3358
3542
|
|
|
@@ -3555,7 +3739,7 @@ When `requiresVerification` is true in the registration response, the customer n
|
|
|
3555
3739
|
```typescript
|
|
3556
3740
|
const auth = await client.registerCustomer({
|
|
3557
3741
|
email: 'customer@example.com',
|
|
3558
|
-
password: '
|
|
3742
|
+
password: 'SecurePass123!',
|
|
3559
3743
|
firstName: 'John',
|
|
3560
3744
|
});
|
|
3561
3745
|
|
|
@@ -4495,11 +4679,9 @@ await client.createAttributeOption(sizeAttr.id, {
|
|
|
4495
4679
|
source: 'GLOBAL',
|
|
4496
4680
|
});
|
|
4497
4681
|
|
|
4498
|
-
// Get options, update, delete
|
|
4682
|
+
// Get options, update the attribute, delete the attribute
|
|
4499
4683
|
const options = await client.getAttributeOptions(colorAttr.id);
|
|
4500
4684
|
await client.updateAttribute(colorAttr.id, { displayType: 'IMAGE_SWATCH' });
|
|
4501
|
-
await client.updateAttributeOption(colorAttr.id, options[0].id, { swatchColor: '#CC0000' });
|
|
4502
|
-
await client.deleteAttributeOption(colorAttr.id, options[0].id);
|
|
4503
4685
|
await client.deleteAttribute(colorAttr.id);
|
|
4504
4686
|
|
|
4505
4687
|
// Storefront: render swatches from product data
|
|
@@ -4510,6 +4692,15 @@ const swatches = getProductSwatches(product);
|
|
|
4510
4692
|
// Returns: [{ attributeName: 'Color', displayType: 'COLOR_SWATCH', options: [{ name: 'Red', swatchColor: '#FF0000', ... }] }]
|
|
4511
4693
|
```
|
|
4512
4694
|
|
|
4695
|
+
> **Editing or deleting a single option is dashboard-only.** `/api/v1/attributes/:id/options`
|
|
4696
|
+
> exposes exactly two verbs — `GET` (list) and `POST` (add). There is no per-option route
|
|
4697
|
+
> on the public API, so `client.updateAttributeOption()` and `client.deleteAttributeOption()`
|
|
4698
|
+
> return **404**. The per-option routes exist only on the dashboard API
|
|
4699
|
+
> (`PUT` / `DELETE /api/stores/:storeId/attributes/:id/options/:optionId`), behind Clerk
|
|
4700
|
+
> auth. Recolour a swatch or drop an option in the dashboard. Everything else on
|
|
4701
|
+
> attributes — create, list, `updateAttribute`, `deleteAttribute`, `getAttributeOptions`,
|
|
4702
|
+
> `createAttributeOption` — works from the SDK as shown above.
|
|
4703
|
+
|
|
4513
4704
|
### Shipping Configuration
|
|
4514
4705
|
|
|
4515
4706
|
```typescript
|
|
@@ -4845,8 +5036,9 @@ vibe-coded site **only if** it has been explicitly published to that
|
|
|
4845
5036
|
connection. Entities with no publish rows are invisible to every vibe-coded
|
|
4846
5037
|
site, including in product responses (the related categories/brands/tags
|
|
4847
5038
|
arrays and the metafields array on each product are filtered the same way).
|
|
4848
|
-
Merchants publish through the dashboard's per-row Platforms cell
|
|
4849
|
-
admin SDK below.
|
|
5039
|
+
Merchants publish through the dashboard's per-row Platforms cell; for
|
|
5040
|
+
categories, tags and brands the admin SDK below does the same thing. Custom
|
|
5041
|
+
fields are the exception — see the note under the snippet.
|
|
4850
5042
|
|
|
4851
5043
|
```typescript
|
|
4852
5044
|
// Publish a product to a sales channel (accepts record ID or vc_* connection ID)
|
|
@@ -4856,24 +5048,38 @@ await client.unpublishProductFromSalesChannel('prod_id', 'vc_conn_id');
|
|
|
4856
5048
|
|
|
4857
5049
|
// Publish a category to a specific vibe-coded site
|
|
4858
5050
|
await client.publishCategoryToVibeCodedSite('cat_id', 'conn_id');
|
|
4859
|
-
// Tag
|
|
5051
|
+
// Tag and brand follow the same shape:
|
|
4860
5052
|
await client.publishTagToVibeCodedSite('tag_id', 'conn_id');
|
|
4861
5053
|
await client.publishBrandToVibeCodedSite('brand_id', 'conn_id');
|
|
4862
|
-
await client.publishMetafieldDefinitionToVibeCodedSite('def_id', 'conn_id');
|
|
4863
5054
|
|
|
4864
5055
|
// Unpublish — entity is no longer visible to that site (but stays visible
|
|
4865
5056
|
// to every other site if no other publishes exist).
|
|
4866
5057
|
await client.unpublishCategoryFromVibeCodedSite('cat_id', 'conn_id');
|
|
4867
5058
|
await client.unpublishTagFromVibeCodedSite('tag_id', 'conn_id');
|
|
4868
5059
|
await client.unpublishBrandFromVibeCodedSite('brand_id', 'conn_id');
|
|
4869
|
-
await client.unpublishMetafieldDefinitionFromVibeCodedSite('def_id', 'conn_id');
|
|
4870
5060
|
|
|
4871
5061
|
// Read which sites an entity is published to via the admin list/get response —
|
|
4872
|
-
// `
|
|
5062
|
+
// `channelPublishes` is included on every list/get for these 4 entity types:
|
|
4873
5063
|
const cat = await client.getCategory('cat_id');
|
|
4874
|
-
cat.
|
|
5064
|
+
cat.channelPublishes; // [{ salesChannel: { id, name, connectionId } }, ...]
|
|
4875
5065
|
```
|
|
4876
5066
|
|
|
5067
|
+
> **Custom fields are the exception — publish them in the dashboard.**
|
|
5068
|
+
> `client.publishMetafieldDefinitionToVibeCodedSite()` and
|
|
5069
|
+
> `client.unpublishMetafieldDefinitionFromVibeCodedSite()` target
|
|
5070
|
+
> `/api/v1/metafield-definitions/:id/publish-vibe-coded`, which the public API does not
|
|
5071
|
+
> expose; both return **404**. Only the dashboard API carries those routes
|
|
5072
|
+
> (`POST /api/stores/:storeId/metafield-definitions/:id/publish` and `…/unpublish`),
|
|
5073
|
+
> behind Clerk auth. **Reading is unaffected** — `getMetafieldDefinitions()` and
|
|
5074
|
+
> `getMetafieldDefinition()` both return `channelPublishes` exactly like the other three
|
|
5075
|
+
> entity types, so you can still see which sites a custom field is published to; you just
|
|
5076
|
+
> cannot change it from the SDK.
|
|
5077
|
+
|
|
5078
|
+
> **`vibeCodedPublishes` and its `connection` sub-key are deprecated.** Both are
|
|
5079
|
+
> still emitted as back-compat aliases of `channelPublishes` / `salesChannel` and
|
|
5080
|
+
> are removed in SDK 2.0. Read `channelPublishes[].salesChannel` in new code —
|
|
5081
|
+
> the customer section below already does.
|
|
5082
|
+
|
|
4877
5083
|
**Cross-account isolation:** publishing only succeeds when the entity and the
|
|
4878
5084
|
target vibe-coded connection both belong to the same account. Cross-account
|
|
4879
5085
|
calls fail with `404 Not Found` (the connection ID is treated as if it doesn't
|
|
@@ -4919,48 +5125,33 @@ not prevent that person from buying on that storefront, and the row comes back
|
|
|
4919
5125
|
the next time they sign in or order there. There is no API to bar a customer
|
|
4920
5126
|
from a sales channel.
|
|
4921
5127
|
|
|
4922
|
-
### Store Team Management
|
|
4923
|
-
|
|
4924
|
-
Each store has its own team with roles (`OWNER`, `MANAGER`, `STAFF`, `VIEWER`) and
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
await client.removeStoreMember('store_id', 'member_id');
|
|
4950
|
-
|
|
4951
|
-
// Manage invitations
|
|
4952
|
-
await client.resendStoreInvitation('store_id', 'inv_id');
|
|
4953
|
-
await client.revokeStoreInvitation('store_id', 'inv_id');
|
|
4954
|
-
|
|
4955
|
-
// Get all stores accessible to the current user (owned + shared)
|
|
4956
|
-
const stores = await client.getMyStores();
|
|
4957
|
-
// [{ id, name, role: 'OWNER', context: 'owner' }, { id, name, role: 'MANAGER', context: 'member' }]
|
|
4958
|
-
|
|
4959
|
-
// Get user's permissions for a specific store
|
|
4960
|
-
const { role, permissions } = await client.getMyStorePermissions('store_id');
|
|
4961
|
-
```
|
|
4962
|
-
|
|
4963
|
-
> **Note:** The previous account-level team methods (`getTeamMembers`, `inviteTeamMember`, etc.) are deprecated. Use the store-level methods above instead.
|
|
5128
|
+
### Store Team Management — dashboard-only
|
|
5129
|
+
|
|
5130
|
+
Each store has its own team with roles (`OWNER`, `MANAGER`, `STAFF`, `VIEWER`) and
|
|
5131
|
+
granular permissions, including per-sales-channel scoping. **Managing it is a dashboard
|
|
5132
|
+
operation. There is no SDK path to it, and this is deliberate.**
|
|
5133
|
+
|
|
5134
|
+
This section previously showed nine store-level team calls. They do not work:
|
|
5135
|
+
`getStoreTeam`, `inviteStoreMember`, `updateStoreMember`, `updateStoreMemberSalesChannels`,
|
|
5136
|
+
`removeStoreMember`, `resendStoreInvitation` and `revokeStoreInvitation` call
|
|
5137
|
+
`/api/v1/stores/:storeId/team…`, and `getMyStores` / `getMyStorePermissions` call
|
|
5138
|
+
`/api/v1/me/stores…`. The public API exposes neither prefix, so every one of them returns
|
|
5139
|
+
**404**.
|
|
5140
|
+
|
|
5141
|
+
**This is not a path typo waiting on a fix.** The real endpoints exist at
|
|
5142
|
+
`/api/stores/:storeId/team…`, guarded by `DashboardOnlyGuard`, which rejects API-key
|
|
5143
|
+
principals _by design_: an API key carries a `storeId` but never a `userId`, and the
|
|
5144
|
+
team service resolves a missing `userId` to an OWNER role. Failing closed at the boundary
|
|
5145
|
+
is what stops an API key from escalating its own team permissions. Pointing the SDK at
|
|
5146
|
+
the correct path would earn a `403` instead of a `404`. Invite, re-scope and remove
|
|
5147
|
+
members in the dashboard.
|
|
5148
|
+
|
|
5149
|
+
> **The older account-level methods are not the workaround.** `getTeamMembers`,
|
|
5150
|
+
> `getTeamInvitations`, `inviteTeamMember`, `resendTeamInvitation`, `revokeTeamInvitation`,
|
|
5151
|
+
> `updateTeamMemberRole` and `removeTeamMember` do still reach `/api/v1/team/…` — but they
|
|
5152
|
+
> manage the **account** team, not a store's, and all seven are `@deprecated`. Their JSDoc
|
|
5153
|
+
> tells you to migrate to the store-level methods named above; ignore that advice, because
|
|
5154
|
+
> those methods 404. Don't build new integrations on either family.
|
|
4964
5155
|
|
|
4965
5156
|
### Email Settings & Templates
|
|
4966
5157
|
|
|
@@ -5030,10 +5221,6 @@ const { summarized, summary } = await client.summarizeBotConversation(conversati
|
|
|
5030
5221
|
### Sync Conflict Resolution
|
|
5031
5222
|
|
|
5032
5223
|
```typescript
|
|
5033
|
-
// Product sync conflicts
|
|
5034
|
-
const conflicts = await client.getSyncConflicts();
|
|
5035
|
-
await client.resolveSyncConflict('conflict_id', 'MERGE'); // or 'CREATE_NEW'
|
|
5036
|
-
|
|
5037
5224
|
// Metafield conflicts
|
|
5038
5225
|
const metafieldConflicts = await client.getMetafieldConflicts();
|
|
5039
5226
|
await client.resolveMetafieldConflict('conflict_id', {
|
|
@@ -5042,6 +5229,12 @@ await client.resolveMetafieldConflict('conflict_id', {
|
|
|
5042
5229
|
await client.ignoreMetafieldConflict('conflict_id');
|
|
5043
5230
|
```
|
|
5044
5231
|
|
|
5232
|
+
> **Product sync conflicts are dashboard-only.** `client.getSyncConflicts()` and
|
|
5233
|
+
> `client.resolveSyncConflict()` call `/api/v1/sync-conflicts`, which the public API does
|
|
5234
|
+
> not expose; both return **404**. Review a product conflict and pick `MERGE` or
|
|
5235
|
+
> `CREATE_NEW` in the dashboard. The three metafield-conflict methods above are a separate
|
|
5236
|
+
> surface (`/api/v1/metafield-conflicts`) and do work with an API key.
|
|
5237
|
+
|
|
5045
5238
|
### OAuth Provider Configuration
|
|
5046
5239
|
|
|
5047
5240
|
```typescript
|
|
@@ -5059,7 +5252,7 @@ await client.deleteOAuthProvider('GOOGLE');
|
|
|
5059
5252
|
|
|
5060
5253
|
### Modifier Groups Management
|
|
5061
5254
|
|
|
5062
|
-
12 admin methods covering group / modifier / attachment CRUD. Money fields are decimal strings on the wire (`priceDelta: "5.00"`). Server-side validation failures arrive as a `MODIFIER_VALIDATION_FAILED` envelope on `BrainerceError.details`.
|
|
5255
|
+
12 admin methods covering group / modifier / attachment CRUD. Money fields are decimal strings on the wire (`priceDelta: "5.00"`). Server-side validation failures arrive as a `MODIFIER_VALIDATION_FAILED` envelope on `BrainerceError.details`, with the per-issue list nested at `err.details.details.errors`.
|
|
5063
5256
|
|
|
5064
5257
|
```typescript
|
|
5065
5258
|
// Group CRUD
|
|
@@ -5920,7 +6113,9 @@ export default function RegisterPage() {
|
|
|
5920
6113
|
<input placeholder="Last Name" value={lastName} onChange={e => setLastName(e.target.value)} required className="border p-2 rounded" />
|
|
5921
6114
|
</div>
|
|
5922
6115
|
<input type="email" placeholder="Email" value={email} onChange={e => setEmail(e.target.value)} required className="w-full border p-2 rounded" />
|
|
5923
|
-
|
|
6116
|
+
{/* The API rejects anything weaker: 8+ chars with a lowercase, an uppercase,
|
|
6117
|
+
a digit AND a special character. `minLength={8}` alone lets a 400 through. */}
|
|
6118
|
+
<input type="password" placeholder="8+ chars, upper + lower + number + symbol" value={password} onChange={e => setPassword(e.target.value)} required minLength={8} pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z0-9]).{8,}" title="At least 8 characters with one uppercase letter, one lowercase letter, one number and one special character" className="w-full border p-2 rounded" />
|
|
5924
6119
|
<button type="submit" disabled={loading} className="w-full bg-black text-white py-3 rounded">
|
|
5925
6120
|
{loading ? 'Creating Account...' : 'Create Account'}
|
|
5926
6121
|
</button>
|
|
@@ -6344,7 +6539,8 @@ const handleAddToCart = async (productId: string, quantity: number) => {
|
|
|
6344
6539
|
}
|
|
6345
6540
|
};
|
|
6346
6541
|
|
|
6347
|
-
// Checkout with toast feedback
|
|
6542
|
+
// Checkout with toast feedback (cash-on-delivery / sandbox store — a store that
|
|
6543
|
+
// takes payment must run the checkout + payment-intent flow instead)
|
|
6348
6544
|
const handleCheckout = async () => {
|
|
6349
6545
|
try {
|
|
6350
6546
|
const order = await client.submitGuestOrder();
|
|
@@ -6420,6 +6616,7 @@ export function useBrainerceAction<T>() {
|
|
|
6420
6616
|
const { execute, isLoading } = useBrainerceAction();
|
|
6421
6617
|
|
|
6422
6618
|
const handlePlaceOrder = () => {
|
|
6619
|
+
// Cash-on-delivery / sandbox only — submitGuestOrder() collects no payment.
|
|
6423
6620
|
execute(() => client.submitGuestOrder(), {
|
|
6424
6621
|
successMessage: 'Order placed successfully!',
|
|
6425
6622
|
onSuccess: (order) => navigate(`/order/${order.orderId}`),
|