@cartbase/storefront 0.9.0 → 0.11.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/package.json +11 -10
- package/src/api/regions.ts +64 -5
- package/src/cart-drawer/index.ts +0 -1
- package/src/checkout/address-error-copy.ts +117 -119
- package/src/checkout/address-form.tsx +6 -0
- package/src/checkout/discount-section.tsx +218 -218
- package/src/checkout/error-copy-codes.ts +63 -0
- package/src/checkout/gift-card-section.tsx +2 -2
- package/src/checkout/index.ts +4 -5
- package/src/checkout/labels.ts +218 -0
- package/src/checkout/payment-button.tsx +372 -372
- package/src/checkout/payment-error-copy.ts +154 -269
- package/src/checkout/promotion-error-copy.ts +91 -124
- package/src/checkout/use-checkout-orchestration.ts +55 -15
- package/src/common/country-flag.tsx +52 -0
- package/src/common/index.ts +1 -0
- package/src/locales/bg.ts +468 -0
- package/src/locales/en.ts +26 -0
- package/src/locales/es.ts +467 -0
- package/src/locales/index.ts +19 -0
- package/src/locales/provider.tsx +63 -0
- package/src/locales/types.ts +49 -0
- package/src/order/index.ts +0 -1
- package/src/primitives/select-field.tsx +19 -3
- package/src/products/index.ts +0 -1
- package/src/reviews-ui/index.ts +0 -1
- package/src/store/index.ts +0 -1
- package/src/tracking/consent-banner.tsx +5 -1
- package/src/cart-drawer/labels-bg.ts +0 -72
- package/src/checkout/labels-bg.ts +0 -128
- package/src/order/labels-bg.ts +0 -39
- package/src/products/labels-bg.ts +0 -35
- package/src/reviews-ui/labels-bg.ts +0 -91
- package/src/store/labels-bg.ts +0 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cartbase/storefront",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Storefront SDK + UI component library for Cartbase stores: typed API client, checkout orchestration, cart drawer, product/catalog components, tracking. Source-shipped TypeScript — add it to transpilePackages.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -103,7 +103,6 @@
|
|
|
103
103
|
"./checkout": "./src/checkout/index.ts",
|
|
104
104
|
"./checkout/context": "./src/checkout/context.tsx",
|
|
105
105
|
"./checkout/labels": "./src/checkout/labels.ts",
|
|
106
|
-
"./checkout/labels-bg": "./src/checkout/labels-bg.ts",
|
|
107
106
|
"./checkout/error-message": "./src/checkout/error-message.tsx",
|
|
108
107
|
"./checkout/stripe-wrapper": "./src/checkout/stripe-wrapper.tsx",
|
|
109
108
|
"./checkout/payment-wrapper": "./src/checkout/payment-wrapper.tsx",
|
|
@@ -127,7 +126,6 @@
|
|
|
127
126
|
"./cart-drawer": "./src/cart-drawer/index.ts",
|
|
128
127
|
"./cart-drawer/context": "./src/cart-drawer/context.tsx",
|
|
129
128
|
"./cart-drawer/labels": "./src/cart-drawer/labels.ts",
|
|
130
|
-
"./cart-drawer/labels-bg": "./src/cart-drawer/labels-bg.ts",
|
|
131
129
|
"./cart-drawer/cart-drawer": "./src/cart-drawer/cart-drawer.tsx",
|
|
132
130
|
"./cart-drawer/header": "./src/cart-drawer/header.tsx",
|
|
133
131
|
"./cart-drawer/promo-banner": "./src/cart-drawer/promo-banner.tsx",
|
|
@@ -151,7 +149,6 @@
|
|
|
151
149
|
"./products": "./src/products/index.ts",
|
|
152
150
|
"./products/context": "./src/products/context.tsx",
|
|
153
151
|
"./products/labels": "./src/products/labels.ts",
|
|
154
|
-
"./products/labels-bg": "./src/products/labels-bg.ts",
|
|
155
152
|
"./products/thumbnail": "./src/products/thumbnail.tsx",
|
|
156
153
|
"./products/preview-price": "./src/products/preview-price.tsx",
|
|
157
154
|
"./products/product-price": "./src/products/product-price.tsx",
|
|
@@ -167,7 +164,6 @@
|
|
|
167
164
|
"./products/product-template": "./src/products/product-template.tsx",
|
|
168
165
|
"./store": "./src/store/index.ts",
|
|
169
166
|
"./store/labels": "./src/store/labels.ts",
|
|
170
|
-
"./store/labels-bg": "./src/store/labels-bg.ts",
|
|
171
167
|
"./store/pagination": "./src/store/pagination.tsx",
|
|
172
168
|
"./store/sort-select": "./src/store/sort-select.tsx",
|
|
173
169
|
"./store/paginated-products": "./src/store/paginated-products.tsx",
|
|
@@ -179,7 +175,6 @@
|
|
|
179
175
|
"./order": "./src/order/index.ts",
|
|
180
176
|
"./order/context": "./src/order/context.tsx",
|
|
181
177
|
"./order/labels": "./src/order/labels.ts",
|
|
182
|
-
"./order/labels-bg": "./src/order/labels-bg.ts",
|
|
183
178
|
"./order/order-confirmation-header": "./src/order/order-confirmation-header.tsx",
|
|
184
179
|
"./order/order-item": "./src/order/order-item.tsx",
|
|
185
180
|
"./order/order-items-list": "./src/order/order-items-list.tsx",
|
|
@@ -195,25 +190,31 @@
|
|
|
195
190
|
"./common/cart-button": "./src/common/cart-button.tsx",
|
|
196
191
|
"./common/cart-button-client": "./src/common/cart-button-client.tsx",
|
|
197
192
|
"./common/delete-button": "./src/common/delete-button.tsx",
|
|
193
|
+
"./common/country-flag": "./src/common/country-flag.tsx",
|
|
198
194
|
"./common/country-select": "./src/common/country-select.tsx",
|
|
199
195
|
"./common/language-select": "./src/common/language-select.tsx",
|
|
200
196
|
"./common/skeleton": "./src/common/skeleton.tsx",
|
|
201
|
-
"./reviews-ui": "./src/reviews-ui/index.ts"
|
|
197
|
+
"./reviews-ui": "./src/reviews-ui/index.ts",
|
|
198
|
+
"./locales": "./src/locales/index.ts",
|
|
199
|
+
"./locales/en": "./src/locales/en.ts",
|
|
200
|
+
"./locales/es": "./src/locales/es.ts",
|
|
201
|
+
"./locales/bg": "./src/locales/bg.ts"
|
|
202
202
|
},
|
|
203
203
|
"dependencies": {
|
|
204
|
-
"@stripe/react-stripe-js": "^6.1.0",
|
|
205
|
-
"@stripe/stripe-js": "^9.1.0",
|
|
206
|
-
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
207
204
|
"@radix-ui/react-accordion": "^1.2.12",
|
|
208
205
|
"@radix-ui/react-collapsible": "^1.1.12",
|
|
209
206
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
207
|
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
210
208
|
"@radix-ui/react-label": "^2.1.8",
|
|
211
209
|
"@radix-ui/react-popover": "^1.1.15",
|
|
212
210
|
"@radix-ui/react-select": "^2.2.6",
|
|
213
211
|
"@radix-ui/react-slot": "^1.2.4",
|
|
214
212
|
"@radix-ui/react-tabs": "^1.1.13",
|
|
213
|
+
"@stripe/react-stripe-js": "^6.1.0",
|
|
214
|
+
"@stripe/stripe-js": "^9.1.0",
|
|
215
215
|
"class-variance-authority": "^0.7.1",
|
|
216
216
|
"clsx": "^2.1.1",
|
|
217
|
+
"flag-icons": "^7.5.0",
|
|
217
218
|
"lucide-react": "^1.8.0",
|
|
218
219
|
"tailwind-merge": "^3.5.0"
|
|
219
220
|
},
|
package/src/api/regions.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @cartbase/storefront/api/regions — regions, currencies, locales.
|
|
2
|
+
* @cartbase/storefront/api/regions — regions, countries, currencies, locales.
|
|
3
3
|
*
|
|
4
4
|
* Catalog-context primitives every storefront resolves at boot: which
|
|
5
|
-
* regions exist (→ `region_id` for the pricing context), which
|
|
6
|
-
*
|
|
7
|
-
* reads (`x-client-id` scoping via RLS). Docs:
|
|
5
|
+
* regions exist (→ `region_id` for the pricing context), which countries the
|
|
6
|
+
* store sells to, which currencies it has enabled, which locales it serves.
|
|
7
|
+
* All endpoints are anon reads (`x-client-id` scoping via RLS). Docs:
|
|
8
|
+
* docs/storefront/regions.md.
|
|
8
9
|
*/
|
|
9
10
|
import type { StorefrontClient } from "./http"
|
|
10
11
|
import type { IsoDateString, ListEnvelope, PaginationQuery } from "./types"
|
|
@@ -12,7 +13,9 @@ import type { IsoDateString, ListEnvelope, PaginationQuery } from "./types"
|
|
|
12
13
|
/**
|
|
13
14
|
* A store region (`barter_commerce.regions` row). NOTE: Cartbase regions carry
|
|
14
15
|
* NO embedded `countries` array on the store surface (divergence from
|
|
15
|
-
* Medusa's Store API — tax scoping is by `tax_regions` server-side).
|
|
16
|
+
* Medusa's Store API — tax scoping is by `tax_regions` server-side). The
|
|
17
|
+
* countries are their own call, `listCountries`, and each row carries the
|
|
18
|
+
* `region_id` it belongs to.
|
|
16
19
|
*/
|
|
17
20
|
export interface StoreRegion {
|
|
18
21
|
id: string
|
|
@@ -38,6 +41,35 @@ export interface StoreCurrency {
|
|
|
38
41
|
updated_at: IsoDateString
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
/**
|
|
45
|
+
* A country a shopper may choose (`barter_commerce.countries` row, the store's
|
|
46
|
+
* own tenant catalogue).
|
|
47
|
+
*/
|
|
48
|
+
export interface StoreCountry {
|
|
49
|
+
/** Lowercase ISO-3166 alpha-2, e.g. "bg" — the value a cart address takes. */
|
|
50
|
+
iso_2: string
|
|
51
|
+
/** What a shopper reads, e.g. "Bulgaria". The code is never shown as copy. */
|
|
52
|
+
display_name: string
|
|
53
|
+
/** The catalogue's uppercase form, e.g. "BULGARIA". */
|
|
54
|
+
name: string
|
|
55
|
+
/** The region this country belongs to, null when the store has not placed it. */
|
|
56
|
+
region_id: string | null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ListCountriesQuery extends PaginationQuery {
|
|
60
|
+
/** Case-insensitive substring match on the displayed name. */
|
|
61
|
+
q?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface CountryListResponse extends ListEnvelope {
|
|
65
|
+
countries: StoreCountry[]
|
|
66
|
+
/**
|
|
67
|
+
* True when this list IS the store's Markets declaration; false when the
|
|
68
|
+
* store has declared nothing and this is the whole ISO catalogue.
|
|
69
|
+
*/
|
|
70
|
+
restricted: boolean
|
|
71
|
+
}
|
|
72
|
+
|
|
41
73
|
export interface ListRegionsQuery extends PaginationQuery {
|
|
42
74
|
/** Case-insensitive substring match on region name. */
|
|
43
75
|
q?: string
|
|
@@ -98,6 +130,33 @@ export async function retrieveRegion(
|
|
|
98
130
|
return client.get(`/api/store/regions/${encodeURIComponent(regionId)}`)
|
|
99
131
|
}
|
|
100
132
|
|
|
133
|
+
/**
|
|
134
|
+
* List the countries a shopper may choose at checkout, ordered by the name
|
|
135
|
+
* they read.
|
|
136
|
+
*
|
|
137
|
+
* THE RULE IS THE PLATFORM'S, NOT THE STOREFRONT'S: the store's own Markets
|
|
138
|
+
* decide where it sells, and a store that has decided nothing yet sells to
|
|
139
|
+
* the whole world. So this answers with the countries the store's regions
|
|
140
|
+
* declare, and with the whole 250-entry catalogue when it declares none;
|
|
141
|
+
* `restricted` says which you got. A storefront therefore never hardcodes a
|
|
142
|
+
* country list, and never has to invent a fallback of its own.
|
|
143
|
+
*
|
|
144
|
+
* `useCheckoutOrchestration` calls this for you when you do not pass it a
|
|
145
|
+
* `countries` list, so a new store offers every country with no wiring at
|
|
146
|
+
* all. Pass your own list only to override the store's answer.
|
|
147
|
+
*
|
|
148
|
+
* Auth: anon (`x-client-id`). Errors: 400 `missing_client_id`,
|
|
149
|
+
* 400 `validation_failed`. Settings: Admin → Settings → Markets → Regions,
|
|
150
|
+
* where a region owns its countries; `limit` 1–300 (default 300, so one
|
|
151
|
+
* call carries the entire catalogue).
|
|
152
|
+
*/
|
|
153
|
+
export async function listCountries(
|
|
154
|
+
client: StorefrontClient,
|
|
155
|
+
query?: ListCountriesQuery
|
|
156
|
+
): Promise<CountryListResponse> {
|
|
157
|
+
return client.get("/api/store/countries", { query: { ...query } })
|
|
158
|
+
}
|
|
159
|
+
|
|
101
160
|
/**
|
|
102
161
|
* List the currencies ENABLED on this store (the `store_currencies` link
|
|
103
162
|
* filters the shared currency catalog). A store with none enabled returns an
|
package/src/cart-drawer/index.ts
CHANGED
|
@@ -1,119 +1,117 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
* Kept verbatim from the production port.
|
|
40
|
-
*/
|
|
41
|
-
const PATTERNS: Array<{ match: RegExp;
|
|
42
|
-
// Email validation — confirmed (in @1click production) as the most common
|
|
43
|
-
// address-save failure shape.
|
|
44
|
-
{
|
|
45
|
-
match: /invalid[\s_]?(?:request:?\s*)?invalid[\s_]?email|invalid[\s_]?email/i,
|
|
46
|
-
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
},
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
if (typeof err === "
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return String(err)
|
|
119
|
-
}
|
|
1
|
+
import type { AddressErrorCopy } from "./labels"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Recognizes an address-save failure and picks the copy for it.
|
|
5
|
+
*
|
|
6
|
+
* THE COPY IS NOT HERE. Until 2026-09-13 this file held thirteen hardcoded
|
|
7
|
+
* BULGARIAN sentences, so every merchant who installed the package showed
|
|
8
|
+
* Bulgarian on the one screen where a shopper is about to pay, with no way
|
|
9
|
+
* to override it. The copy now lives in the checkout label pack like all
|
|
10
|
+
* other copy, which means it is English by default and translated by
|
|
11
|
+
* whichever locale the store mounts.
|
|
12
|
+
*
|
|
13
|
+
* WHAT STAYS HERE IS THE RECOGNITION, and it stays on purpose: the codes
|
|
14
|
+
* and the patterns match the API's own error text, which is English wire
|
|
15
|
+
* format, not user-facing copy. Translating the matcher would break it.
|
|
16
|
+
*
|
|
17
|
+
* Ported from `@1click/ui/src/checkout/address-error-copy.ts` (v2.3.1) and
|
|
18
|
+
* adapted to the Cartbase error envelope: `StoreApiError` carries a
|
|
19
|
+
* structured `code` (POST /api/store/carts/:id, docs/storefront/carts.md),
|
|
20
|
+
* so recognition is code-first and the substring patterns are the fallback
|
|
21
|
+
* for network failures and any legacy wire.
|
|
22
|
+
*
|
|
23
|
+
* The mapping is deliberately narrow: only failures we recognize get their
|
|
24
|
+
* own sentence, and everything else gets the generic one rather than
|
|
25
|
+
* half-translated technical text.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** Cartbase cart-update error codes, mapped to a copy key. Code-first. */
|
|
29
|
+
export const ADDRESS_CODE_KEYS: Record<string, keyof AddressErrorCopy> = {
|
|
30
|
+
validation_failed: "validationFailed",
|
|
31
|
+
invalid_region: "invalidRegion",
|
|
32
|
+
region_required: "regionRequired",
|
|
33
|
+
cart_completed: "cartCompleted",
|
|
34
|
+
cart_not_found: "cartNotFound",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Patterns matched in order, first match wins: most specific first,
|
|
39
|
+
* generic last. Case-insensitive. Kept verbatim from the production port.
|
|
40
|
+
*/
|
|
41
|
+
const PATTERNS: Array<{ match: RegExp; key: keyof AddressErrorCopy }> = [
|
|
42
|
+
// Email validation — confirmed (in @1click production) as the most common
|
|
43
|
+
// address-save failure shape.
|
|
44
|
+
{
|
|
45
|
+
match: /invalid[\s_]?(?:request:?\s*)?invalid[\s_]?email|invalid[\s_]?email/i,
|
|
46
|
+
key: "invalidEmail",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
match:
|
|
50
|
+
/invalid[\s_]?postal|invalid[\s_]?post(?:code|al[\s_]?code)|postal[\s_]?code[\s_]?(?:is[\s_]?)?invalid/i,
|
|
51
|
+
key: "invalidPostalCode",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
match: /invalid[\s_]?phone|phone[\s_]?(?:number[\s_]?)?(?:is[\s_]?)?invalid/i,
|
|
55
|
+
key: "invalidPhone",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
match:
|
|
59
|
+
/invalid[\s_]?country|country[\s_]?(?:code[\s_]?)?(?:is[\s_]?)?invalid|no[\s_]?region|invalid[\s_]?region/i,
|
|
60
|
+
key: "invalidCountry",
|
|
61
|
+
},
|
|
62
|
+
// Cart already completed — race with order placement.
|
|
63
|
+
{
|
|
64
|
+
match:
|
|
65
|
+
/cart[\s_]?(?:is[\s_]?)?already[\s_]?completed|already[\s_]?an[\s_]?order|completed[\s_]?cart/i,
|
|
66
|
+
key: "cartCompleted",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
match: /cart[\s_]?(?:not[\s_]?found|does[\s_]?not[\s_]?exist|expired)/i,
|
|
70
|
+
key: "cartNotFound",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
match: /failed[\s_]?to[\s_]?fetch|network[\s_]?error|networkerror|econnreset|etimedout/i,
|
|
74
|
+
key: "network",
|
|
75
|
+
},
|
|
76
|
+
// The Next.js canned Server Components production message — surface a
|
|
77
|
+
// clean fallback instead of the English placeholder. (The Cartbase SDK
|
|
78
|
+
// throws from the browser, so this only fires for stores that proxy the
|
|
79
|
+
// save through a server action.)
|
|
80
|
+
{
|
|
81
|
+
match: /server[\s_]?components[\s_]?render|digest property is included/i,
|
|
82
|
+
key: "generic",
|
|
83
|
+
},
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
/** Which copy an error asks for, independent of language. */
|
|
87
|
+
export function addressErrorKey(err: unknown): keyof AddressErrorCopy {
|
|
88
|
+
if (err && typeof err === "object") {
|
|
89
|
+
const code = (err as { code?: unknown }).code
|
|
90
|
+
if (typeof code === "string" && ADDRESS_CODE_KEYS[code]) return ADDRESS_CODE_KEYS[code]
|
|
91
|
+
}
|
|
92
|
+
const raw = extractMessage(err)
|
|
93
|
+
if (!raw) return "generic"
|
|
94
|
+
for (const { match, key } of PATTERNS) {
|
|
95
|
+
if (match.test(raw)) return key
|
|
96
|
+
}
|
|
97
|
+
return "generic"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Turn any thrown or returned address-save error into the sentence a
|
|
102
|
+
* shopper reads, in the store's own language.
|
|
103
|
+
*/
|
|
104
|
+
export function translateAddressError(err: unknown, copy: AddressErrorCopy): string {
|
|
105
|
+
return copy[addressErrorKey(err)]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function extractMessage(err: unknown): string {
|
|
109
|
+
if (!err) return ""
|
|
110
|
+
if (typeof err === "string") return err
|
|
111
|
+
if (err instanceof Error) return err.message
|
|
112
|
+
if (typeof err === "object" && err !== null) {
|
|
113
|
+
const e = err as { message?: string; code?: string; type?: string }
|
|
114
|
+
return [e.message, e.code, e.type].filter(Boolean).join(" ")
|
|
115
|
+
}
|
|
116
|
+
return String(err)
|
|
117
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { ChangeEvent } from "react"
|
|
|
4
4
|
|
|
5
5
|
import type { CartAddress } from "../api/carts"
|
|
6
6
|
import type { CustomerAddress } from "../api/customers"
|
|
7
|
+
import { CountryFlag } from "../common/country-flag"
|
|
7
8
|
import { Field } from "../primitives/field"
|
|
8
9
|
import { SelectField } from "../primitives/select-field"
|
|
9
10
|
import { AddressSelect } from "./address-select"
|
|
@@ -127,6 +128,11 @@ export function CheckoutAddressForm({
|
|
|
127
128
|
value={formData["shipping_address.country_code"] ?? ""}
|
|
128
129
|
onChange={onChange}
|
|
129
130
|
required
|
|
131
|
+
// The chosen country's flag, inside the control. It stays a
|
|
132
|
+
// NATIVE select on purpose: a custom listbox would draw a flag
|
|
133
|
+
// per row but lose `autocomplete="country"` and the phone's own
|
|
134
|
+
// picker, and a checkout does not trade autofill for decoration.
|
|
135
|
+
leading={<CountryFlag code={formData["shipping_address.country_code"]} />}
|
|
130
136
|
>
|
|
131
137
|
<option value="" disabled />
|
|
132
138
|
{countries.map((c) => (
|