@cartbase/storefront 0.20.1 → 0.22.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 +274 -258
- package/src/api/checkout.ts +15 -0
- package/src/api/http.ts +10 -0
- package/src/api/integrations.ts +121 -0
- package/src/api/store.ts +21 -0
- package/src/cart-drawer/payment-badges.tsx +36 -96
- package/src/checkout/card-offer.ts +68 -0
- package/src/checkout/carrier-marks.ts +53 -0
- package/src/checkout/checkout-client.tsx +71 -13
- package/src/checkout/checkout-error-screen.tsx +113 -0
- package/src/checkout/discount-section.tsx +88 -56
- package/src/checkout/fulfillment-option.ts +30 -0
- package/src/checkout/index.ts +41 -6
- package/src/checkout/labels.ts +98 -0
- package/src/checkout/line-item-card.tsx +35 -83
- package/src/checkout/mobile-checkout-bottom-bar.tsx +132 -0
- package/src/checkout/mobile-checkout-top-bar.tsx +94 -0
- package/src/checkout/mobile-order-summary-body.tsx +172 -0
- package/src/checkout/order-summary.tsx +85 -179
- package/src/checkout/payment-button.tsx +25 -8
- package/src/checkout/payment-method-list.tsx +51 -13
- package/src/checkout/payment-wrapper.tsx +57 -13
- package/src/checkout/pickup-option.ts +35 -0
- package/src/checkout/pickup-point-selector.tsx +372 -0
- package/src/checkout/pigeon-office-selector.tsx +379 -0
- package/src/checkout/shipping-method-list.tsx +102 -10
- package/src/checkout/summary-math.ts +152 -0
- package/src/checkout/use-checkout-funnel.ts +303 -0
- package/src/checkout/use-checkout-orchestration.ts +384 -30
- package/src/common/icons/cartbase-mark.ts +9 -0
- package/src/common/icons/payment-marks.ts +73 -0
- package/src/common/icons/social-marks.ts +61 -0
- package/src/common/index.ts +15 -0
- package/src/common/payment-icons.tsx +54 -0
- package/src/common/powered-by-cartbase.tsx +47 -0
- package/src/common/social-links.tsx +65 -0
- package/src/lib/stripe-env.ts +25 -0
- package/src/locales/bg.ts +39 -0
- package/src/locales/es.ts +37 -0
- package/src/store/labels.ts +10 -0
- package/src/tracking/attribution.ts +83 -0
- package/src/tracking/consent.ts +53 -0
- package/src/tracking/events.ts +113 -0
- package/src/tracking/fbq.ts +48 -0
- package/src/tracking/gtag.ts +32 -0
- package/src/tracking/index.ts +32 -1
- package/src/tracking/once.ts +137 -0
- package/src/tracking/rybbit-events.ts +42 -0
- package/src/tracking/ttq.ts +24 -0
- package/src/tracking/types.ts +34 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react"
|
|
4
|
+
|
|
5
|
+
import { reportCheckoutError } from "../api/checkout"
|
|
6
|
+
import type { StorefrontClient } from "../api/http"
|
|
7
|
+
import { useCheckoutLabels } from "./context"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* CheckoutErrorScreen — what a shopper sees when the checkout PAGE fails,
|
|
11
|
+
* as opposed to a call inside it.
|
|
12
|
+
*
|
|
13
|
+
* Next renders a route's `error.tsx` in place of the page when anything in
|
|
14
|
+
* it throws, a server component included, through the RSC bridge. Without
|
|
15
|
+
* one, a checkout that throws shows the framework's own error page: a
|
|
16
|
+
* shopper at the moment of paying is handed a stack trace or a blank
|
|
17
|
+
* screen, their cart looks lost, and nobody is told. The kit's own
|
|
18
|
+
* failures already reach the merchant's checkout error log; this closes
|
|
19
|
+
* the one class that never did, because the component that reports them
|
|
20
|
+
* was the component that died.
|
|
21
|
+
*
|
|
22
|
+
* Mount it as the route's boundary:
|
|
23
|
+
*
|
|
24
|
+
* // src/app/checkout/error.tsx
|
|
25
|
+
* "use client"
|
|
26
|
+
* import { CheckoutErrorScreen } from "@cartbase/storefront/checkout"
|
|
27
|
+
* import { storeClient } from "@/lib/store"
|
|
28
|
+
*
|
|
29
|
+
* export default function CheckoutError(props) {
|
|
30
|
+
* return <CheckoutErrorScreen {...props} client={storeClient} />
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* The copy comes from the label pack when a `CheckoutProvider` stands
|
|
34
|
+
* above the boundary (a provider in the route's LAYOUT does; one inside
|
|
35
|
+
* the page does not, because the page is what Next replaced). Pass the
|
|
36
|
+
* three strings for a store whose provider sits inside the page.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export type CheckoutErrorScreenProps = {
|
|
40
|
+
/** Next's error boundary argument. */
|
|
41
|
+
error: Error & { digest?: string }
|
|
42
|
+
/** Next's retry: re-renders the route's tree. */
|
|
43
|
+
reset: () => void
|
|
44
|
+
/**
|
|
45
|
+
* The SDK transport. With it the failure reaches the merchant's checkout
|
|
46
|
+
* error log; without it the screen still renders, and nobody hears.
|
|
47
|
+
*/
|
|
48
|
+
client?: StorefrontClient
|
|
49
|
+
/** The cart the shopper was checking out, when the store knows it. */
|
|
50
|
+
cartId?: string
|
|
51
|
+
/** Copy override, for a boundary that stands outside the provider. */
|
|
52
|
+
title?: string
|
|
53
|
+
body?: string
|
|
54
|
+
retryLabel?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function CheckoutErrorScreen({
|
|
58
|
+
error,
|
|
59
|
+
reset,
|
|
60
|
+
client,
|
|
61
|
+
cartId,
|
|
62
|
+
title,
|
|
63
|
+
body,
|
|
64
|
+
retryLabel,
|
|
65
|
+
}: CheckoutErrorScreenProps) {
|
|
66
|
+
const labels = useCheckoutLabels()
|
|
67
|
+
// React runs effects twice in development's strict mode, and a retry
|
|
68
|
+
// that fails again remounts this component: the ref keeps one failure to
|
|
69
|
+
// one report instead of filling the merchant's log with copies.
|
|
70
|
+
const reportedRef = useRef<string | null>(null)
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!client) return
|
|
74
|
+
const key = error.digest ?? error.message ?? "unknown"
|
|
75
|
+
if (reportedRef.current === key) return
|
|
76
|
+
reportedRef.current = key
|
|
77
|
+
|
|
78
|
+
void reportCheckoutError(client, {
|
|
79
|
+
error_type: "error_boundary_checkout",
|
|
80
|
+
message: error.message || String(error),
|
|
81
|
+
...(cartId ? { cart_id: cartId } : {}),
|
|
82
|
+
context: {
|
|
83
|
+
digest: error.digest ?? null,
|
|
84
|
+
err_name: error.name ?? null,
|
|
85
|
+
// Enough of the stack to find the component, short enough to stay
|
|
86
|
+
// inside the sink's limit.
|
|
87
|
+
err_stack: error.stack?.slice(0, 1500) ?? null,
|
|
88
|
+
path:
|
|
89
|
+
typeof window !== "undefined" ? window.location.pathname : null,
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
}, [client, cartId, error])
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="min-h-[60vh] flex items-center justify-center px-5 py-16">
|
|
96
|
+
<div className="max-w-md text-center">
|
|
97
|
+
<h1 className="text-xl font-semibold text-foreground mb-2">
|
|
98
|
+
{title ?? labels.errorTitle}
|
|
99
|
+
</h1>
|
|
100
|
+
<p className="text-sm text-muted-foreground mb-5">
|
|
101
|
+
{body ?? labels.errorBody}
|
|
102
|
+
</p>
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
onClick={reset}
|
|
106
|
+
className="inline-flex items-center justify-center h-11 px-5 rounded-xl bg-foreground text-card text-sm font-semibold transition-colors hover:bg-foreground/90"
|
|
107
|
+
>
|
|
108
|
+
{retryLabel ?? labels.errorRetry}
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -62,6 +62,13 @@ type DiscountSectionProps = {
|
|
|
62
62
|
/** The SDK transport — the promotions route is called through it. */
|
|
63
63
|
client: StorefrontClient
|
|
64
64
|
cart: Cart
|
|
65
|
+
/**
|
|
66
|
+
* Show the field itself instead of the door that opens it. The checkout
|
|
67
|
+
* passes this on the phone, where the door would be the second tap
|
|
68
|
+
* inside an already-closed summary bar and a shopper holding a code
|
|
69
|
+
* would never find it.
|
|
70
|
+
*/
|
|
71
|
+
alwaysOpen?: boolean
|
|
65
72
|
/** Receives the decorated cart returned by a successful apply. */
|
|
66
73
|
onCartChange?: (cart: Cart) => void
|
|
67
74
|
}
|
|
@@ -69,6 +76,7 @@ type DiscountSectionProps = {
|
|
|
69
76
|
export function DiscountSection({
|
|
70
77
|
client,
|
|
71
78
|
cart,
|
|
79
|
+
alwaysOpen = false,
|
|
72
80
|
onCartChange,
|
|
73
81
|
}: DiscountSectionProps) {
|
|
74
82
|
const labels = useCheckoutLabels()
|
|
@@ -98,6 +106,45 @@ export function DiscountSection({
|
|
|
98
106
|
}
|
|
99
107
|
}
|
|
100
108
|
|
|
109
|
+
const field = (
|
|
110
|
+
<div className={cn("flex gap-2", !alwaysOpen && "mt-3")}>
|
|
111
|
+
<input
|
|
112
|
+
type="text"
|
|
113
|
+
value={code}
|
|
114
|
+
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
|
115
|
+
onKeyDown={(e) => {
|
|
116
|
+
if (e.key === "Enter") {
|
|
117
|
+
e.preventDefault()
|
|
118
|
+
void handleApply()
|
|
119
|
+
}
|
|
120
|
+
}}
|
|
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"
|
|
123
|
+
/>
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
onClick={handleApply}
|
|
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"
|
|
129
|
+
>
|
|
130
|
+
{labels.addCode}
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if (alwaysOpen) {
|
|
136
|
+
return (
|
|
137
|
+
<div>
|
|
138
|
+
<p className="text-xs font-medium text-muted-foreground mb-1.5">
|
|
139
|
+
{labels.discountCode}
|
|
140
|
+
</p>
|
|
141
|
+
{field}
|
|
142
|
+
{error && <p className="text-xs text-destructive mt-2">{error}</p>}
|
|
143
|
+
{promotions.length > 0 && <AppliedList promotions={promotions} />}
|
|
144
|
+
</div>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
101
148
|
return (
|
|
102
149
|
<div>
|
|
103
150
|
<button
|
|
@@ -152,67 +199,52 @@ export function DiscountSection({
|
|
|
152
199
|
</span>
|
|
153
200
|
</button>
|
|
154
201
|
|
|
155
|
-
{open &&
|
|
156
|
-
<div className="mt-3 flex gap-2">
|
|
157
|
-
<input
|
|
158
|
-
type="text"
|
|
159
|
-
value={code}
|
|
160
|
-
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
|
161
|
-
placeholder="PROMO2024"
|
|
162
|
-
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"
|
|
163
|
-
/>
|
|
164
|
-
<button
|
|
165
|
-
type="button"
|
|
166
|
-
onClick={handleApply}
|
|
167
|
-
disabled={loading || !code.trim()}
|
|
168
|
-
className="px-5 h-11 text-sm font-semibold bg-foreground text-card rounded-xl hover:bg-foreground/90 transition-all disabled:opacity-40"
|
|
169
|
-
>
|
|
170
|
-
{labels.addCode}
|
|
171
|
-
</button>
|
|
172
|
-
</div>
|
|
173
|
-
)}
|
|
202
|
+
{open && field}
|
|
174
203
|
|
|
175
204
|
{error && <p className="text-xs text-destructive mt-2">{error}</p>}
|
|
176
205
|
|
|
177
|
-
{promotions.length > 0 &&
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
206
|
+
{promotions.length > 0 && <AppliedList promotions={promotions} />}
|
|
207
|
+
</div>
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The codes already on the cart, with what each one takes off. */
|
|
212
|
+
function AppliedList({ promotions }: { promotions: AppliedPromotion[] }) {
|
|
213
|
+
return (
|
|
214
|
+
<div className="mt-3 space-y-1.5">
|
|
215
|
+
{promotions.map((p) => (
|
|
216
|
+
<div
|
|
217
|
+
key={p.id}
|
|
218
|
+
className="flex items-center justify-between px-4 py-2.5 bg-success/10 border border-success/20 rounded-lg"
|
|
219
|
+
>
|
|
220
|
+
<div className="flex items-center gap-2">
|
|
221
|
+
<svg
|
|
222
|
+
className="w-4 h-4 text-success"
|
|
223
|
+
fill="none"
|
|
224
|
+
viewBox="0 0 24 24"
|
|
225
|
+
stroke="currentColor"
|
|
226
|
+
strokeWidth={2}
|
|
183
227
|
>
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
<span className="text-sm font-medium text-success">
|
|
203
|
-
{p.application_method?.type === "percentage"
|
|
204
|
-
? `-${p.application_method.value}%`
|
|
205
|
-
: p.application_method?.value && p.application_method?.currency_code
|
|
206
|
-
? `-${convertToLocale({
|
|
207
|
-
amount: +p.application_method.value,
|
|
208
|
-
currency_code: p.application_method.currency_code,
|
|
209
|
-
})}`
|
|
210
|
-
: ""}
|
|
211
|
-
</span>
|
|
212
|
-
</div>
|
|
213
|
-
))}
|
|
228
|
+
<path
|
|
229
|
+
strokeLinecap="round"
|
|
230
|
+
strokeLinejoin="round"
|
|
231
|
+
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
232
|
+
/>
|
|
233
|
+
</svg>
|
|
234
|
+
<span className="text-sm font-semibold text-success">{p.code}</span>
|
|
235
|
+
</div>
|
|
236
|
+
<span className="text-sm font-medium text-success">
|
|
237
|
+
{p.application_method?.type === "percentage"
|
|
238
|
+
? `-${p.application_method.value}%`
|
|
239
|
+
: p.application_method?.value && p.application_method?.currency_code
|
|
240
|
+
? `-${convertToLocale({
|
|
241
|
+
amount: +p.application_method.value,
|
|
242
|
+
currency_code: p.application_method.currency_code,
|
|
243
|
+
})}`
|
|
244
|
+
: ""}
|
|
245
|
+
</span>
|
|
214
246
|
</div>
|
|
215
|
-
)}
|
|
247
|
+
))}
|
|
216
248
|
</div>
|
|
217
249
|
)
|
|
218
250
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { StoreShippingOption } from "../api/checkout"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WHICH DESTINATION a shipping option books, read from the option itself.
|
|
5
|
+
*
|
|
6
|
+
* Cartbase writes the merchant's choice as `data.fulfillment_option` (the
|
|
7
|
+
* Shipping option drawer's "To an Econt office", "To a BoxNow locker" and
|
|
8
|
+
* the rest, from the carrier registry). The kit was ported from a platform
|
|
9
|
+
* that put the same value at `data.id`, and nothing on this side ever wrote
|
|
10
|
+
* that key, so on a Cartbase store every reader of it saw null: no office
|
|
11
|
+
* or locker picker ever opened, no carrier mark ever keyed, and a cart
|
|
12
|
+
* could reach Buy with an office option and no office chosen (found live on
|
|
13
|
+
* the Demo Store, 2026-09-18).
|
|
14
|
+
*
|
|
15
|
+
* So the Cartbase key is read first and the ported one stays as a fallback,
|
|
16
|
+
* because a cart carried over from that platform still speaks it.
|
|
17
|
+
*/
|
|
18
|
+
export function fulfillmentOptionId(
|
|
19
|
+
option: Pick<StoreShippingOption, "data"> | null | undefined
|
|
20
|
+
): string | null {
|
|
21
|
+
const data = option?.data as
|
|
22
|
+
| { fulfillment_option?: unknown; id?: unknown }
|
|
23
|
+
| null
|
|
24
|
+
| undefined
|
|
25
|
+
if (typeof data?.fulfillment_option === "string" && data.fulfillment_option) {
|
|
26
|
+
return data.fulfillment_option
|
|
27
|
+
}
|
|
28
|
+
if (typeof data?.id === "string" && data.id) return data.id
|
|
29
|
+
return null
|
|
30
|
+
}
|
package/src/checkout/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// @cartbase/storefront/checkout — barrel export.
|
|
2
2
|
//
|
|
3
|
-
// Ported from `@1click/ui/src/checkout/index.ts` (v2.3.1).
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// Extra modules beyond the pre-declared export list
|
|
8
|
-
// compare-addresses) are exported from here only.
|
|
3
|
+
// Ported from `@1click/ui/src/checkout/index.ts` (v2.3.1). CheckoutLineItem
|
|
4
|
+
// has no standalone file here: its flat row lives inside order-summary.tsx
|
|
5
|
+
// and is re-exported below. The mobile bars are exported too, so a store
|
|
6
|
+
// with its own checkout layout composes the same phone summary the default
|
|
7
|
+
// layout mounts. Extra modules beyond the pre-declared export list
|
|
8
|
+
// (geocode, compare-addresses) are exported from here only.
|
|
9
9
|
|
|
10
10
|
export {
|
|
11
11
|
CheckoutProvider,
|
|
@@ -21,6 +21,11 @@ export {
|
|
|
21
21
|
useStripeScope,
|
|
22
22
|
} from "./stripe-wrapper"
|
|
23
23
|
export { PaymentWrapper } from "./payment-wrapper"
|
|
24
|
+
export {
|
|
25
|
+
resolveCardOffer,
|
|
26
|
+
type CardEntryLike,
|
|
27
|
+
type CardOffer,
|
|
28
|
+
} from "./card-offer"
|
|
24
29
|
export { PaymentButton } from "./payment-button"
|
|
25
30
|
export {
|
|
26
31
|
EcontOfficeSelector,
|
|
@@ -30,16 +35,41 @@ export {
|
|
|
30
35
|
BoxNowLockerSelector,
|
|
31
36
|
type BoxNowLocker,
|
|
32
37
|
} from "./boxnow-locker-selector"
|
|
38
|
+
export {
|
|
39
|
+
PigeonOfficeSelector,
|
|
40
|
+
type PigeonOffice,
|
|
41
|
+
} from "./pigeon-office-selector"
|
|
42
|
+
export {
|
|
43
|
+
PickupPointSelector,
|
|
44
|
+
type PickupPoint,
|
|
45
|
+
} from "./pickup-point-selector"
|
|
46
|
+
export { pickupOptionOf, type PickupOption } from "./pickup-option"
|
|
47
|
+
export { carrierMarks } from "./carrier-marks"
|
|
33
48
|
export { AddressSelect } from "./address-select"
|
|
34
49
|
export { CompanyDetails } from "./company-details"
|
|
35
50
|
export { DiscountSection } from "./discount-section"
|
|
36
51
|
export { GiftCardSection } from "./gift-card-section"
|
|
37
52
|
export { LineItemCard } from "./line-item-card"
|
|
38
53
|
export { OrderSummary, CheckoutLineItem } from "./order-summary"
|
|
54
|
+
export { MobileCheckoutTopBar } from "./mobile-checkout-top-bar"
|
|
55
|
+
export { MobileCheckoutBottomBar } from "./mobile-checkout-bottom-bar"
|
|
56
|
+
export { MobileOrderSummaryBody } from "./mobile-order-summary-body"
|
|
57
|
+
export {
|
|
58
|
+
checkoutTotals,
|
|
59
|
+
amountDueAfterTender,
|
|
60
|
+
isNothingToPay,
|
|
61
|
+
isCompanyOrder,
|
|
62
|
+
type CheckoutTotals,
|
|
63
|
+
type CheckoutTotalsInput,
|
|
64
|
+
} from "./summary-math"
|
|
39
65
|
export { CheckoutAddressForm } from "./address-form"
|
|
40
66
|
export { CheckoutShippingMethodList } from "./shipping-method-list"
|
|
41
67
|
export { CheckoutPaymentMethodList } from "./payment-method-list"
|
|
42
68
|
export { CheckoutClient } from "./checkout-client"
|
|
69
|
+
export {
|
|
70
|
+
CheckoutErrorScreen,
|
|
71
|
+
type CheckoutErrorScreenProps,
|
|
72
|
+
} from "./checkout-error-screen"
|
|
43
73
|
export {
|
|
44
74
|
useCheckoutOrchestration,
|
|
45
75
|
resolveOrderConfirmedPath,
|
|
@@ -47,6 +77,11 @@ export {
|
|
|
47
77
|
type CheckoutLogError,
|
|
48
78
|
type PaymentProviderLike,
|
|
49
79
|
} from "./use-checkout-orchestration"
|
|
80
|
+
export {
|
|
81
|
+
useCheckoutFunnel,
|
|
82
|
+
type UseCheckoutFunnelOptions,
|
|
83
|
+
} from "./use-checkout-funnel"
|
|
84
|
+
export { fulfillmentOptionId } from "./fulfillment-option"
|
|
50
85
|
export {
|
|
51
86
|
translatePaymentError,
|
|
52
87
|
translateGiftCardError,
|
package/src/checkout/labels.ts
CHANGED
|
@@ -46,6 +46,15 @@ export type CheckoutLabels = {
|
|
|
46
46
|
addCode: string
|
|
47
47
|
|
|
48
48
|
// States / empty / disabled
|
|
49
|
+
/**
|
|
50
|
+
* The checkout with nothing to check out. A shopper who lands here with
|
|
51
|
+
* an empty cart, or after their order was placed in another tab, used to
|
|
52
|
+
* be bounced to the home page without a word, which reads as the store
|
|
53
|
+
* losing their order. The page says it instead, and offers the way back.
|
|
54
|
+
*/
|
|
55
|
+
cartEmptyTitle: string
|
|
56
|
+
cartEmptyText: string
|
|
57
|
+
cartEmptyBack: string
|
|
49
58
|
deliveryDisabled: string
|
|
50
59
|
noShippingOptions: string
|
|
51
60
|
paymentDisabled: string
|
|
@@ -80,6 +89,18 @@ export type CheckoutLabels = {
|
|
|
80
89
|
shipping: string
|
|
81
90
|
shippingCalc: string
|
|
82
91
|
shippingFree: string
|
|
92
|
+
/**
|
|
93
|
+
* What a line that costs nothing says in place of its price: a gift a
|
|
94
|
+
* promotion added, or a line a discount took to zero.
|
|
95
|
+
*
|
|
96
|
+
* A SEPARATE label from `shippingFree` on purpose, and not a saving of
|
|
97
|
+
* one string: in Bulgarian the shipping row reads "Безплатна", agreeing
|
|
98
|
+
* with доставка, and a product line reads "Безплатно". One label would
|
|
99
|
+
* print the wrong gender on one of the two rows for every Bulgarian
|
|
100
|
+
* store, which is exactly the kind of detail a shopper reads as
|
|
101
|
+
* carelessness at the moment of paying.
|
|
102
|
+
*/
|
|
103
|
+
lineFree: string
|
|
83
104
|
tax: string
|
|
84
105
|
/**
|
|
85
106
|
* What the tax is, in the store's words. OPTIONAL and undefined by
|
|
@@ -124,7 +145,19 @@ export type CheckoutLabels = {
|
|
|
124
145
|
|
|
125
146
|
// Place order button
|
|
126
147
|
placeOrder: string
|
|
148
|
+
/** The button on an order with nothing left to pay. */
|
|
149
|
+
completeOrder: string
|
|
127
150
|
selectPaymentMethod: string
|
|
151
|
+
/** What stands where the tender list would be on a covered order. */
|
|
152
|
+
nothingToPay: string
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The checkout's own error screen (`CheckoutErrorScreen`), shown when
|
|
156
|
+
* the page itself fails rather than a call inside it.
|
|
157
|
+
*/
|
|
158
|
+
errorTitle: string
|
|
159
|
+
errorBody: string
|
|
160
|
+
errorRetry: string
|
|
128
161
|
|
|
129
162
|
/**
|
|
130
163
|
* Processing-state messages shown beneath the spinning Buy button
|
|
@@ -141,6 +174,27 @@ export type CheckoutLabels = {
|
|
|
141
174
|
processingCard: string[]
|
|
142
175
|
processingCod: string[]
|
|
143
176
|
|
|
177
|
+
/**
|
|
178
|
+
* The pickup picker, for every carrier (2026-09-18). One picker means one
|
|
179
|
+
* set of words: a shopper choosing a Speedy office and a shopper choosing
|
|
180
|
+
* an Econt office are doing the same thing, and the carrier is already
|
|
181
|
+
* named on the row above by its own mark. The three per-carrier sets below
|
|
182
|
+
* stay for stores that still mount the old components; new work reads
|
|
183
|
+
* these.
|
|
184
|
+
*/
|
|
185
|
+
pickupLoading: string
|
|
186
|
+
pickupNearestOffices: string
|
|
187
|
+
pickupNearestLockers: string
|
|
188
|
+
pickupSearchOffice: string
|
|
189
|
+
pickupSearchLocker: string
|
|
190
|
+
pickupSearchOfficePlaceholder: string
|
|
191
|
+
pickupSearchLockerPlaceholder: string
|
|
192
|
+
pickupNoResults: string
|
|
193
|
+
pickupChange: string
|
|
194
|
+
pickupUnavailable: string
|
|
195
|
+
/** Why Buy is still disabled: an office option with no office named. */
|
|
196
|
+
pickupRequired: string
|
|
197
|
+
|
|
144
198
|
// Econt office selector
|
|
145
199
|
econtLoadingOffices: string
|
|
146
200
|
econtNearestOffices: string
|
|
@@ -158,6 +212,16 @@ export type CheckoutLabels = {
|
|
|
158
212
|
boxnowChange: string
|
|
159
213
|
boxnowUnavailable: string
|
|
160
214
|
boxnowNoLockersInCity: string
|
|
215
|
+
|
|
216
|
+
// Pigeon Express office selector
|
|
217
|
+
pigeonLoadingOffices: string
|
|
218
|
+
pigeonNearestOffices: string
|
|
219
|
+
pigeonSearchAnother: string
|
|
220
|
+
pigeonSearchPlaceholder: string
|
|
221
|
+
pigeonNoResults: string
|
|
222
|
+
pigeonChange: string
|
|
223
|
+
pigeonUnavailable: string
|
|
224
|
+
pigeonNoOfficesInCity: string
|
|
161
225
|
/**
|
|
162
226
|
* What a shopper reads when saving the address fails. It used to be
|
|
163
227
|
* hardcoded BULGARIAN inside `address-error-copy.ts` with no way to
|
|
@@ -287,6 +351,9 @@ export const defaultCheckoutLabels: CheckoutLabels = {
|
|
|
287
351
|
addToOrder: "+ Add to order",
|
|
288
352
|
addCode: "Apply",
|
|
289
353
|
|
|
354
|
+
cartEmptyTitle: "Your cart is empty",
|
|
355
|
+
cartEmptyText: "Add something to it and your order starts here.",
|
|
356
|
+
cartEmptyBack: "Back to the shop",
|
|
290
357
|
deliveryDisabled:
|
|
291
358
|
"Enter your delivery address to see available shipping options.",
|
|
292
359
|
noShippingOptions: "No shipping options available for your address.",
|
|
@@ -312,6 +379,7 @@ export const defaultCheckoutLabels: CheckoutLabels = {
|
|
|
312
379
|
shipping: "Shipping",
|
|
313
380
|
shippingCalc: "Calculated at checkout",
|
|
314
381
|
shippingFree: "Free",
|
|
382
|
+
lineFree: "Free",
|
|
315
383
|
// The row NAME, neutral on purpose: it sits beside a number, so it must
|
|
316
384
|
// say something, but "VAT incl." names one tax regime and asserts that
|
|
317
385
|
// prices include it. That is Bulgaria's answer, not Germany's, and not any
|
|
@@ -347,7 +415,14 @@ export const defaultCheckoutLabels: CheckoutLabels = {
|
|
|
347
415
|
companyAddress: "Company address",
|
|
348
416
|
|
|
349
417
|
placeOrder: "Place order",
|
|
418
|
+
completeOrder: "Complete order",
|
|
350
419
|
selectPaymentMethod: "Select a payment method",
|
|
420
|
+
nothingToPay: "Your order is fully covered. There is nothing left to pay.",
|
|
421
|
+
|
|
422
|
+
errorTitle: "Something went wrong at checkout",
|
|
423
|
+
errorBody:
|
|
424
|
+
"Your cart is safe. Try again, and if it keeps happening, get in touch.",
|
|
425
|
+
errorRetry: "Try again",
|
|
351
426
|
|
|
352
427
|
processingCard: [
|
|
353
428
|
"Connecting to the bank...",
|
|
@@ -361,6 +436,18 @@ export const defaultCheckoutLabels: CheckoutLabels = {
|
|
|
361
436
|
"Attending to every detail...",
|
|
362
437
|
],
|
|
363
438
|
|
|
439
|
+
pickupLoading: "Loading pickup points...",
|
|
440
|
+
pickupNearestOffices: "Nearest offices",
|
|
441
|
+
pickupNearestLockers: "Nearest lockers",
|
|
442
|
+
pickupSearchOffice: "Search for another office",
|
|
443
|
+
pickupSearchLocker: "Search for another locker",
|
|
444
|
+
pickupSearchOfficePlaceholder: "Search by name, city, or address...",
|
|
445
|
+
pickupSearchLockerPlaceholder: "Search by name, city, or address...",
|
|
446
|
+
pickupNoResults: "Nothing found",
|
|
447
|
+
pickupChange: "Change",
|
|
448
|
+
pickupUnavailable: "Pickup points are unavailable right now",
|
|
449
|
+
pickupRequired: "Choose a pickup point",
|
|
450
|
+
|
|
364
451
|
econtLoadingOffices: "Loading offices...",
|
|
365
452
|
econtNearestOffices: "Nearest offices",
|
|
366
453
|
econtSearchAnother: "Search for another office",
|
|
@@ -378,6 +465,17 @@ export const defaultCheckoutLabels: CheckoutLabels = {
|
|
|
378
465
|
"BoxNow is temporarily unavailable, please choose a different shipping method.",
|
|
379
466
|
boxnowNoLockersInCity:
|
|
380
467
|
"No BoxNow lockers found in your city. Please choose a different shipping method.",
|
|
468
|
+
|
|
469
|
+
pigeonLoadingOffices: "Loading offices...",
|
|
470
|
+
pigeonNearestOffices: "Nearest offices",
|
|
471
|
+
pigeonSearchAnother: "Search for another office",
|
|
472
|
+
pigeonSearchPlaceholder: "Search by name, city, or address...",
|
|
473
|
+
pigeonNoResults: "No offices found for",
|
|
474
|
+
pigeonChange: "Change",
|
|
475
|
+
pigeonUnavailable:
|
|
476
|
+
"Pigeon Express is temporarily unavailable, please choose a different shipping method.",
|
|
477
|
+
pigeonNoOfficesInCity:
|
|
478
|
+
"No Pigeon Express offices found in your city. Please choose a different shipping method.",
|
|
381
479
|
addressErrors: {
|
|
382
480
|
generic:
|
|
383
481
|
"We could not save your address. Please check the details and try again.",
|