@cartbase/storefront 0.24.0 → 0.24.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartbase/storefront",
3
- "version": "0.24.0",
3
+ "version": "0.24.1",
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": {
@@ -146,6 +146,12 @@ export interface ListPaymentProvidersQuery {
146
146
  * checkout; without it cart-dependent hide rules cannot match here.
147
147
  */
148
148
  cart_id?: string
149
+ /**
150
+ * Evaluate payment rules for the delivery option the shopper just chose.
151
+ * This avoids waiting for the cart preference write before refreshing the
152
+ * server-authoritative payment list.
153
+ */
154
+ shipping_option_id?: string
149
155
  }
150
156
 
151
157
  /** A connected PROCESSOR entry of GET /api/store/payment-providers. */
@@ -197,6 +203,12 @@ export const isMethodEntry = (
197
203
 
198
204
  export interface PaymentProviderListResponse extends ListEnvelope {
199
205
  payment_providers: StorePaymentEntry[]
206
+ /**
207
+ * Server-evaluated payment choices keyed by delivery option. CheckoutClient
208
+ * uses this to react synchronously during rapid shipping switches without
209
+ * reproducing the rules engine in the browser.
210
+ */
211
+ payment_providers_by_shipping_option?: Record<string, StorePaymentEntry[]>
200
212
  }
201
213
 
202
214
  /**
@@ -59,8 +59,10 @@ type CheckoutClientProps = {
59
59
  client: StorefrontClient
60
60
  cart: Cart
61
61
  customer: StoreCustomer | null
62
- availableShippingMethods: StoreShippingOption[] | null
62
+ availableShippingMethods: StoreShippingOption[] | null
63
63
  availablePaymentMethods: PaymentProviderLike[] | null
64
+ /** Server-evaluated tender lists keyed by delivery option id. */
65
+ paymentMethodsByShippingOption?: Record<string, PaymentProviderLike[]>
64
66
  /** Card and wallet marks returned by `GET /api/store/payment-marks`. */
65
67
  paymentMarks?: readonly PaymentMethod[]
66
68
  /** Store-owned terms notice, including links to its content pages. */
@@ -113,8 +115,9 @@ export function CheckoutClient({
113
115
  client,
114
116
  cart,
115
117
  customer,
116
- availableShippingMethods,
118
+ availableShippingMethods,
117
119
  availablePaymentMethods,
120
+ paymentMethodsByShippingOption,
118
121
  paymentMarks,
119
122
  termsContent,
120
123
  countryCode,
@@ -136,8 +139,9 @@ export function CheckoutClient({
136
139
  client,
137
140
  cart,
138
141
  customer,
139
- availableShippingMethods,
140
- availablePaymentMethods,
142
+ availableShippingMethods,
143
+ availablePaymentMethods,
144
+ paymentMethodsByShippingOption,
141
145
  countryCode,
142
146
  countries,
143
147
  paymentMethodFilter,
@@ -107,7 +107,7 @@ export function DiscountSection({
107
107
  }
108
108
 
109
109
  const field = (
110
- <div className={cn("flex gap-2", !alwaysOpen && "mt-3")}>
110
+ <div className={cn("grid grid-cols-[minmax(0,1fr)_auto] gap-2", !alwaysOpen && "mt-3")}>
111
111
  <input
112
112
  type="text"
113
113
  value={code}
@@ -119,13 +119,13 @@ export function DiscountSection({
119
119
  }
120
120
  }}
121
121
  placeholder="PROMO2024"
122
- className="flex-1 h-11 px-4 text-sm bg-card border border-border rounded-xl focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all"
122
+ className="w-full min-w-0 h-11 px-4 text-sm bg-card border border-border rounded-xl focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all"
123
123
  />
124
124
  <button
125
125
  type="button"
126
126
  onClick={handleApply}
127
127
  disabled={loading || !code.trim()}
128
- className="px-5 h-11 text-sm font-semibold bg-foreground text-card rounded-xl hover:bg-foreground/90 transition-all disabled:opacity-40"
128
+ className="px-4 h-11 text-sm font-semibold bg-foreground text-card rounded-xl hover:bg-foreground/90 transition-all disabled:opacity-40"
129
129
  >
130
130
  {labels.addCode}
131
131
  </button>
@@ -150,7 +150,7 @@ export function DiscountSection({
150
150
  <button
151
151
  type="button"
152
152
  onClick={() => setOpen(!open)}
153
- className="flex items-center gap-3 w-full p-4 rounded-xl border border-border bg-muted/50 hover:bg-muted transition-colors"
153
+ className="grid grid-cols-[2.5rem_minmax(0,1fr)_auto] items-center gap-3 w-full p-4 rounded-xl border border-border bg-card hover:border-primary transition-colors"
154
154
  >
155
155
  <div className="w-10 h-10 rounded-full bg-gradient-to-br from-warning to-warning/70 flex items-center justify-center flex-shrink-0">
156
156
  <svg
@@ -172,13 +172,15 @@ export function DiscountSection({
172
172
  />
173
173
  </svg>
174
174
  </div>
175
- <div className="flex-1 text-left">
176
- <p className="text-sm font-semibold text-foreground">
175
+ <div className="min-w-0 text-left">
176
+ <p className="text-sm font-semibold leading-tight text-foreground truncate">
177
177
  {labels.discountCode}
178
178
  </p>
179
- <p className="text-xs text-muted-foreground">{labels.discountSub}</p>
179
+ <p className="hidden min-[380px]:block text-xs text-muted-foreground truncate">
180
+ {labels.discountSub}
181
+ </p>
180
182
  </div>
181
- <span className="text-sm font-medium text-foreground flex items-center gap-1.5">
183
+ <span className="shrink-0 whitespace-nowrap text-sm font-medium text-foreground flex items-center gap-1.5">
182
184
  {labels.addCode}
183
185
  <svg
184
186
  className={cn(
@@ -100,7 +100,7 @@ export function GiftCardSection({
100
100
  <button
101
101
  type="button"
102
102
  onClick={() => setOpen(!open)}
103
- className="flex items-center gap-3 w-full p-4 rounded-xl border border-border bg-muted/50 hover:bg-muted transition-colors"
103
+ className="grid grid-cols-[2.5rem_minmax(0,1fr)_auto] items-center gap-3 w-full p-4 rounded-xl border border-border bg-card hover:border-primary transition-colors"
104
104
  >
105
105
  <div className="w-10 h-10 rounded-full bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center flex-shrink-0">
106
106
  {/* Gift icon */}
@@ -118,13 +118,15 @@ export function GiftCardSection({
118
118
  />
119
119
  </svg>
120
120
  </div>
121
- <div className="flex-1 text-left">
122
- <p className="text-sm font-semibold text-foreground">
121
+ <div className="min-w-0 text-left">
122
+ <p className="text-sm font-semibold leading-tight text-foreground truncate">
123
123
  {labels.giftCard}
124
124
  </p>
125
- <p className="text-xs text-muted-foreground">{labels.giftCardSub}</p>
125
+ <p className="hidden min-[380px]:block text-xs text-muted-foreground truncate">
126
+ {labels.giftCardSub}
127
+ </p>
126
128
  </div>
127
- <span className="text-sm font-medium text-foreground flex items-center gap-1.5">
129
+ <span className="shrink-0 whitespace-nowrap text-sm font-medium text-foreground flex items-center gap-1.5">
128
130
  {labels.addCode}
129
131
  <svg
130
132
  className={cn(
@@ -146,19 +148,19 @@ export function GiftCardSection({
146
148
  </button>
147
149
 
148
150
  {open && (
149
- <div className="mt-3 flex gap-2">
151
+ <div className="mt-3 grid grid-cols-[minmax(0,1fr)_auto] gap-2">
150
152
  <input
151
153
  type="text"
152
154
  value={code}
153
155
  onChange={(e) => setCode(e.target.value.toUpperCase())}
154
156
  placeholder={labels.giftCardPlaceholder}
155
- className="flex-1 h-11 px-4 text-sm bg-card border border-border rounded-xl focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all"
157
+ className="w-full min-w-0 h-11 px-4 text-sm bg-card border border-border rounded-xl focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all"
156
158
  />
157
159
  <button
158
160
  type="button"
159
161
  onClick={handleApply}
160
162
  disabled={loading || !code.trim()}
161
- className="px-5 h-11 text-sm font-semibold bg-foreground text-card rounded-xl hover:bg-foreground/90 transition-all disabled:opacity-40"
163
+ className="px-4 h-11 text-sm font-semibold bg-foreground text-card rounded-xl hover:bg-foreground/90 transition-all disabled:opacity-40"
162
164
  >
163
165
  {labels.addCode}
164
166
  </button>
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Serialize a rapidly changing preference while coalescing clicks that have
3
+ * not started yet. The write in flight finishes; then only the newest queued
4
+ * value is written. This guarantees that the final durable value is the last
5
+ * user intent even when network responses complete unpredictably.
6
+ */
7
+ export function createLatestWriteQueue<T>() {
8
+ let queued: { value: T; write: (value: T) => Promise<unknown> } | null = null
9
+ let running = false
10
+
11
+ const drain = async () => {
12
+ if (running) return
13
+ running = true
14
+ try {
15
+ while (queued) {
16
+ const next = queued
17
+ queued = null
18
+ try {
19
+ await next.write(next.value)
20
+ } catch {
21
+ // Persistence is best effort. A newer queued value must still run,
22
+ // and checkout completion independently validates the final choice.
23
+ }
24
+ }
25
+ } finally {
26
+ running = false
27
+ // A push cannot interleave with synchronous code above, but keeping the
28
+ // final check makes the queue safe if its implementation later changes.
29
+ if (queued) void drain()
30
+ }
31
+ }
32
+
33
+ return {
34
+ push(value: T, write: (value: T) => Promise<unknown>) {
35
+ queued = { value, write }
36
+ void drain()
37
+ },
38
+ }
39
+ }
@@ -50,7 +50,7 @@ export function MobileCheckoutTopBar({
50
50
  })
51
51
 
52
52
  return (
53
- <div className="sm:hidden bg-muted border-y border-border">
53
+ <div className="sm:hidden bg-card border-y border-border">
54
54
  <button
55
55
  type="button"
56
56
  onClick={() => setOpen((v) => !v)}
@@ -146,11 +146,11 @@ export function PickupPointSelector({
146
146
  <button
147
147
  key={`${point.provider}-${point.id}`}
148
148
  type="button"
149
- onClick={() => choose(point)}
150
- className={cn(
151
- "flex items-start gap-3 w-full px-3.5 py-3 text-left transition-all duration-150 rounded-lg",
152
- "bg-card hover:bg-muted"
153
- )}
149
+ onClick={() => choose(point)}
150
+ className={cn(
151
+ "flex items-start gap-3 w-full px-3.5 py-3 text-left transition-colors duration-150 rounded-lg border border-transparent",
152
+ "bg-card hover:border-primary"
153
+ )}
154
154
  >
155
155
  <div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0 mt-0.5">
156
156
  <svg
@@ -175,8 +175,8 @@ export function PickupPointSelector({
175
175
  </div>
176
176
 
177
177
  <div className="flex-1 min-w-0">
178
- <p className="text-sm font-medium text-foreground leading-tight">
179
- {point.name}
178
+ <p className="text-sm font-medium text-foreground leading-tight uppercase">
179
+ {point.name}
180
180
  </p>
181
181
  <p className="text-xs text-muted-foreground mt-0.5">
182
182
  {point.secondary_name ||
@@ -223,7 +223,7 @@ export function PickupPointSelector({
223
223
  d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
224
224
  />
225
225
  </svg>
226
- <p className="text-sm font-medium text-primary">{selectedPoint.name}</p>
226
+ <p className="text-sm font-medium text-primary uppercase">{selectedPoint.name}</p>
227
227
  <button
228
228
  type="button"
229
229
  onClick={() => choose(null)}
@@ -195,10 +195,10 @@ export function CheckoutShippingMethodList({
195
195
  <div
196
196
  key={option.id}
197
197
  className={cn(
198
- "rounded-lg border overflow-hidden transition-colors duration-150",
199
- selected
200
- ? "border-primary bg-primary/5"
201
- : "border-border bg-card hover:border-muted-foreground"
198
+ "rounded-lg border overflow-hidden transition-colors duration-150",
199
+ selected
200
+ ? "border-primary bg-primary/5"
201
+ : "border-border bg-card hover:border-primary"
202
202
  )}
203
203
  >
204
204
  <button
@@ -235,13 +235,13 @@ export function CheckoutShippingMethodList({
235
235
  </span>
236
236
  )}
237
237
  <div className="flex-1 min-w-0">
238
- <span className="text-sm font-medium text-foreground">
239
- {option.name}
240
- </span>
238
+ <span className="text-sm font-medium text-foreground uppercase">
239
+ {option.name}
240
+ </span>
241
241
  {selected && isPickup && pickup?.selectedPoint && (
242
- <p className="text-xs text-muted-foreground mt-0.5">
243
- {pickup.selectedPoint.name}
244
- </p>
242
+ <p className="text-xs text-muted-foreground mt-0.5 uppercase">
243
+ {pickup.selectedPoint.name}
244
+ </p>
245
245
  )}
246
246
  </div>
247
247
  <span
@@ -25,8 +25,9 @@ import {
25
25
  prepareCheckout,
26
26
  refreshPaymentIfTerminal as refreshPaymentIfTerminalApi,
27
27
  reportCheckoutError,
28
- syncPaymentAmount as syncPaymentAmountApi,
29
- isMethodEntry,
28
+ syncPaymentAmount as syncPaymentAmountApi,
29
+ isMethodEntry,
30
+ listPaymentProviders,
30
31
  type PrepareCheckoutInput,
31
32
  type RefreshPaymentResult,
32
33
  type StorePaymentEntry,
@@ -54,7 +55,8 @@ import { useCheckoutLabels, useOrderConfirmedPath } from "./context"
54
55
  import { fulfillmentOptionId } from "./fulfillment-option"
55
56
  import { useCheckoutFunnel } from "./use-checkout-funnel"
56
57
  import { amountDueAfterTender, isNothingToPay } from "./summary-math"
57
- import { forgetFiredEvents } from "../tracking/once"
58
+ import { forgetFiredEvents } from "../tracking/once"
59
+ import { createLatestWriteQueue } from "./latest-write-queue"
58
60
 
59
61
  /**
60
62
  * useCheckoutOrchestration — single source of truth for checkout-page
@@ -122,8 +124,13 @@ export type UseCheckoutOrchestrationOptions = {
122
124
  client: StorefrontClient
123
125
  cart: Cart
124
126
  customer: StoreCustomer | null
125
- availableShippingMethods: StoreShippingOption[] | null
126
- availablePaymentMethods: PaymentProviderLike[] | null
127
+ availableShippingMethods: StoreShippingOption[] | null
128
+ availablePaymentMethods: PaymentProviderLike[] | null
129
+ /**
130
+ * Server-evaluated payment availability keyed by delivery option. When
131
+ * supplied, a shipping click changes the tender list synchronously.
132
+ */
133
+ paymentMethodsByShippingOption?: Record<string, PaymentProviderLike[]>
127
134
  /** Default country code when the cart has no shipping address yet. */
128
135
  countryCode?: string
129
136
  /**
@@ -287,8 +294,9 @@ export function useCheckoutOrchestration({
287
294
  client,
288
295
  cart,
289
296
  customer,
290
- availableShippingMethods,
291
- availablePaymentMethods,
297
+ availableShippingMethods,
298
+ availablePaymentMethods,
299
+ paymentMethodsByShippingOption,
292
300
  countryCode = "",
293
301
  countries,
294
302
  paymentMethodFilter,
@@ -868,18 +876,49 @@ export function useCheckoutOrchestration({
868
876
  [availableShippingMethods]
869
877
  )
870
878
 
871
- const selectedShippingOption = useMemo(
879
+ const selectedShippingOption = useMemo(
872
880
  () =>
873
881
  shippingMethods.find((sm) => sm.id === selectedShippingMethod) ?? null,
874
882
  [shippingMethods, selectedShippingMethod]
875
- )
876
-
877
- const effectiveAvailablePaymentMethods = useMemo(
878
- () =>
879
- paymentMethodFilter
880
- ? paymentMethodFilter(availablePaymentMethods, selectedShippingOption)
881
- : availablePaymentMethods,
882
- [paymentMethodFilter, availablePaymentMethods, selectedShippingOption]
883
+ )
884
+
885
+ // Payment rules can depend on the selected delivery option. The host's
886
+ // initial list is server-rendered; after a delivery choice is remembered
887
+ // on the cart, ask the same server-side rules door for the new list. A
888
+ // sequence guard prevents a slower earlier choice from winning a rapid
889
+ // switch between carriers.
890
+ const [serverAvailablePaymentMethods, setServerAvailablePaymentMethods] =
891
+ useState<PaymentProviderLike[] | null>(availablePaymentMethods)
892
+ const paymentListingRequest = useRef(0)
893
+ useEffect(() => {
894
+ const mapped = selectedShippingMethod
895
+ ? paymentMethodsByShippingOption?.[selectedShippingMethod]
896
+ : undefined
897
+ setServerAvailablePaymentMethods(mapped ?? availablePaymentMethods)
898
+ }, [availablePaymentMethods, paymentMethodsByShippingOption, selectedShippingMethod])
899
+ const refreshPaymentMethods = useCallback(async (shippingOptionId: string) => {
900
+ const request = ++paymentListingRequest.current
901
+ try {
902
+ const result = await listPaymentProviders(client, {
903
+ cart_id: cart.id,
904
+ region_id: cart.region_id ?? undefined,
905
+ shipping_option_id: shippingOptionId,
906
+ })
907
+ if (request === paymentListingRequest.current) {
908
+ setServerAvailablePaymentMethods(result.payment_providers)
909
+ }
910
+ } catch {
911
+ // Keep the last good listing. Completion re-checks the same rule on
912
+ // the server, so a transient refresh failure cannot bypass it.
913
+ }
914
+ }, [client, cart.id, cart.region_id])
915
+
916
+ const effectiveAvailablePaymentMethods = useMemo(
917
+ () =>
918
+ paymentMethodFilter
919
+ ? paymentMethodFilter(serverAvailablePaymentMethods, selectedShippingOption)
920
+ : serverAvailablePaymentMethods,
921
+ [paymentMethodFilter, serverAvailablePaymentMethods, selectedShippingOption]
883
922
  )
884
923
 
885
924
  // Calculated-rate price resolution. Cartbase serves flat prices today
@@ -1039,10 +1078,10 @@ export function useCheckoutOrchestration({
1039
1078
  // - null → no prediction; render whatever the cart says
1040
1079
  // - 0 → predict no fee (toggling to a fee-less tender)
1041
1080
  // - positive → predict the fee at this amount
1042
- const [optimisticMethodFee, setOptimisticMethodFee] = useState<number | null>(
1043
- null
1044
- )
1045
-
1081
+ const [optimisticMethodFee, setOptimisticMethodFee] = useState<number | null>(
1082
+ null
1083
+ )
1084
+
1046
1085
  // Payment tab selection is client state only pre-Buy. No payment
1047
1086
  // session is created until Buy click — this eliminates the entire
1048
1087
  // class of session-rotation / amount-drift / iframe-remount bugs
@@ -1096,15 +1135,28 @@ export function useCheckoutOrchestration({
1096
1135
  * are the checkout's own notes to itself, and prepare-checkout overwrites
1097
1136
  * the real ones from them.
1098
1137
  */
1099
- const rememberChoice = useCallback(
1100
- (patch: Record<string, unknown>) => {
1101
- void updateCart(client, cart.id, { metadata: patch }).catch(() => {
1102
- // Best effort: forgetting a choice on reload is a worse checkout,
1103
- // never a broken one, so a failure here must not reach the shopper.
1104
- })
1138
+ const rememberChoice = useCallback(
1139
+ (patch: Record<string, unknown>) => {
1140
+ return updateCart(client, cart.id, { metadata: patch })
1141
+ .then(() => true)
1142
+ .catch(() => {
1143
+ // Best effort: forgetting a choice on reload is a worse checkout,
1144
+ // never a broken one, so a failure here must not reach the shopper.
1145
+ return false
1146
+ })
1105
1147
  },
1106
- [client, cart.id]
1107
- )
1148
+ [client, cart.id]
1149
+ )
1150
+ const choiceWrites = useMemo(
1151
+ () => createLatestWriteQueue<Record<string, unknown>>(),
1152
+ []
1153
+ )
1154
+ const enqueueChoiceWrite = useCallback(
1155
+ (patch: Record<string, unknown>) => {
1156
+ choiceWrites.push(patch, rememberChoice)
1157
+ },
1158
+ [choiceWrites, rememberChoice]
1159
+ )
1108
1160
 
1109
1161
  // Shipping selection stays client state for the ORDER (no
1110
1162
  // addShippingMethod call, no metadata-clear updateCart: those wrote to the
@@ -1113,9 +1165,23 @@ export function useCheckoutOrchestration({
1113
1165
  // CHOICE, under our own key, so a reload restores it.
1114
1166
  const handleSelectShipping = useCallback(
1115
1167
  (id: string) => {
1116
- setShippingError(null)
1117
- setSelectedShippingMethod(id)
1118
- rememberChoice({ _checkout_shipping_option: id })
1168
+ setShippingError(null)
1169
+ setSelectedShippingMethod(id)
1170
+
1171
+ // The availability map came from the server with the checkout's first
1172
+ // read. Selecting one of its answers is immediate and deterministic;
1173
+ // no browser-side rule is being guessed. Older refreshes are invalidated
1174
+ // before they can repaint a newer click.
1175
+ const mappedPaymentMethods = paymentMethodsByShippingOption?.[id]
1176
+ if (mappedPaymentMethods) {
1177
+ paymentListingRequest.current += 1
1178
+ setServerAvailablePaymentMethods(mappedPaymentMethods)
1179
+ } else {
1180
+ // Backward compatibility for hosts/platforms without the map: ask the
1181
+ // rules endpoint about this prospective option immediately, without
1182
+ // waiting for the preference write to make a round trip first.
1183
+ void refreshPaymentMethods(id)
1184
+ }
1119
1185
 
1120
1186
  // Optimistic shipping cost — paint the totals row immediately so
1121
1187
  // the customer sees the right number before any network call.
@@ -1135,14 +1201,20 @@ export function useCheckoutOrchestration({
1135
1201
  // our own note is cleared beside the state.
1136
1202
  setSelectedPickupPoint(null)
1137
1203
  setPickupKeys(null)
1138
- rememberChoice({
1139
- _checkout_shipping_option: id,
1140
- _checkout_pickup_point: null,
1141
- _checkout_pickup_keys: null,
1142
- })
1143
- },
1144
- [shippingMethods, calculatedPricesMap, rememberChoice]
1145
- )
1204
+ enqueueChoiceWrite({
1205
+ _checkout_shipping_option: id,
1206
+ _checkout_pickup_point: null,
1207
+ _checkout_pickup_keys: null,
1208
+ })
1209
+ },
1210
+ [
1211
+ shippingMethods,
1212
+ calculatedPricesMap,
1213
+ paymentMethodsByShippingOption,
1214
+ refreshPaymentMethods,
1215
+ enqueueChoiceWrite,
1216
+ ]
1217
+ )
1146
1218
 
1147
1219
  /**
1148
1220
  * The chosen pickup point, remembered the moment it is chosen. The keys
@@ -1154,13 +1226,13 @@ export function useCheckoutOrchestration({
1154
1226
  (point: PickupPoint | null, keys: PickupPointKeys | null) => {
1155
1227
  setSelectedPickupPoint(point)
1156
1228
  setPickupKeys(keys)
1157
- rememberChoice({
1158
- _checkout_pickup_point: point,
1159
- _checkout_pickup_keys: keys,
1160
- })
1161
- },
1162
- [rememberChoice]
1163
- )
1229
+ enqueueChoiceWrite({
1230
+ _checkout_pickup_point: point,
1231
+ _checkout_pickup_keys: keys,
1232
+ })
1233
+ },
1234
+ [enqueueChoiceWrite]
1235
+ )
1164
1236
 
1165
1237
  // ── Delivery readiness ──────────────────────────────────────────────
1166
1238
  const selectedFulfillmentOptionId = useMemo(() => {
@@ -1197,19 +1269,18 @@ export function useCheckoutOrchestration({
1197
1269
  (cart?.shipping_methods?.length ?? 0) > 0) &&
1198
1270
  pickupSatisfied
1199
1271
 
1200
- // Reconcile paymentTab with currently-available methods. When the
1201
- // store's paymentMethodFilter strips a method in response to a shipping
1202
- // change (e.g. BoxNow no COD), the previously selected tab can point
1203
- // to a method that's no longer rendered leaving the remaining tab's
1204
- // radio looking unselected and the form collapsed.
1205
- useEffect(() => {
1206
- if (!deliveryReady) return
1207
- if (paymentTab === "cod" && !hasCod && hasCard) {
1272
+ // Reconcile paymentTab with currently-available methods. The server map
1273
+ // changes the rendered list synchronously on a shipping click; this effect
1274
+ // then moves selection and fee state onto the first valid tender. It does
1275
+ // not wait for delivery readiness: a locker may still need its point, but
1276
+ // its payment rule is already true.
1277
+ useEffect(() => {
1278
+ if (paymentTab === "cod" && !hasCod && hasCard) {
1208
1279
  handlePaymentTab("card")
1209
1280
  } else if (paymentTab === "card" && !hasCard && hasCod) {
1210
1281
  handlePaymentTab("cod")
1211
1282
  }
1212
- }, [deliveryReady, paymentTab, hasCard, hasCod, handlePaymentTab])
1283
+ }, [paymentTab, hasCard, hasCod, handlePaymentTab])
1213
1284
 
1214
1285
  const handlePaymentElementChange = useCallback(
1215
1286
  (_e: { complete: boolean; selectedMethod: string | null }) => {
package/src/locales/bg.ts CHANGED
@@ -182,12 +182,12 @@ export const bg: StorefrontLocale = {
182
182
  remove: "Премахни",
183
183
  secureCheckout: "Защитено плащане",
184
184
 
185
- giftCard: "Карта за подарък",
186
- giftCardSub: "Използвайте карта за подарък",
185
+ giftCard: "Подаръчен ваучер",
186
+ giftCardSub: "Въведете код за ваучер",
187
187
  giftCardPlaceholder: "GIFT-XXXX-XXXX",
188
- giftCardApplied: "Карти за подарък",
188
+ giftCardApplied: "Подаръчни ваучери",
189
189
  giftCardRemaining: "Остава за плащане",
190
- giftCardEnding: "Карта, завършваща на",
190
+ giftCardEnding: "Ваучер, завършващ на",
191
191
 
192
192
  companyInvoice: "Фактура за фирма",
193
193
  companyName: "Име на фирма",