@tribe-nest/forge 3.25.0 → 3.29.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 +1 -1
- package/src/client/_tests/localeHeader.spec.tsx +98 -0
- package/src/client/activeFunnel.ts +35 -0
- package/src/client/activeLocale.ts +34 -0
- package/src/client/createForgeClient.ts +22 -0
- package/src/contexts/CartContext.tsx +84 -1
- package/src/contexts/_tests/CartContext.spec.tsx +210 -0
- package/src/data/queries/useCheckouts.ts +83 -0
- package/src/data/queries/useForms.ts +48 -3
- package/src/data/queries/usePageActions.ts +2 -2
- package/src/data/queries/useWebsite.ts +7 -0
- package/src/i18n/CONVENTION.md +184 -0
- package/src/i18n/_tests/translationKeys.spec.ts +153 -0
- package/src/i18n/de.json +960 -0
- package/src/i18n/en.json +960 -0
- package/src/i18n/index.ts +125 -0
- package/src/index.ts +21 -0
- package/src/provider/ForgeAppProvider.tsx +2 -1
- package/src/provider/SiteConfigProvider.tsx +11 -0
- package/src/ui/headless/checkout/_tests/cartResume.spec.ts +291 -0
- package/src/ui/headless/checkout/_tests/useCartResume.spec.tsx +294 -0
- package/src/ui/headless/checkout/resumedCoupon.ts +61 -0
- package/src/ui/headless/checkout/useCartResume.ts +340 -0
- package/src/ui/headless/checkout/useCheckout.ts +59 -14
- package/src/ui/headless/event/useEventCheckout.ts +3 -0
- package/src/ui/headless/funnel/Funnel.tsx +13 -0
- package/src/ui/headless/index.ts +18 -0
- package/src/ui/index.ts +2 -0
- package/src/ui/shell/TribeNestApp.tsx +36 -27
- package/src/ui/styled/AccountDashboard.tsx +197 -93
- package/src/ui/styled/AiAgentWidget.tsx +10 -6
- package/src/ui/styled/AudioPlayer.tsx +14 -6
- package/src/ui/styled/BlogCategory.tsx +4 -2
- package/src/ui/styled/BlogComments.tsx +27 -20
- package/src/ui/styled/BlogList.tsx +5 -3
- package/src/ui/styled/BlogPost.tsx +14 -11
- package/src/ui/styled/BundleConfirmation.tsx +86 -46
- package/src/ui/styled/CancellationTerms.tsx +3 -1
- package/src/ui/styled/Cart.tsx +32 -14
- package/src/ui/styled/ChatRoom.tsx +65 -20
- package/src/ui/styled/Checkout.tsx +189 -88
- package/src/ui/styled/CheckoutConfirmation.tsx +66 -35
- package/src/ui/styled/CoachingBooking.tsx +71 -44
- package/src/ui/styled/CoachingConfirmation.tsx +56 -19
- package/src/ui/styled/CoachingDetail.tsx +15 -11
- package/src/ui/styled/CohortPage.tsx +10 -8
- package/src/ui/styled/ConfirmSubscription.tsx +14 -8
- package/src/ui/styled/ContactForm.tsx +31 -12
- package/src/ui/styled/CookieConsent.tsx +19 -15
- package/src/ui/styled/CourseAccess.tsx +54 -47
- package/src/ui/styled/CourseCheckout.tsx +56 -38
- package/src/ui/styled/CourseConfirmation.tsx +39 -17
- package/src/ui/styled/CourseDetail.tsx +21 -15
- package/src/ui/styled/CoursesGrid.tsx +4 -2
- package/src/ui/styled/CurrencySwitcher.tsx +4 -1
- package/src/ui/styled/DiscountCode.tsx +22 -18
- package/src/ui/styled/DonationButton.tsx +52 -17
- package/src/ui/styled/DonationPage.tsx +4 -4
- package/src/ui/styled/EmailListForm.tsx +22 -9
- package/src/ui/styled/EventConfirmation.tsx +37 -14
- package/src/ui/styled/EventCountdown.tsx +10 -6
- package/src/ui/styled/EventDetail.tsx +8 -6
- package/src/ui/styled/EventSeriesDetail.tsx +18 -13
- package/src/ui/styled/EventTickets.tsx +311 -125
- package/src/ui/styled/EventWaitlist.tsx +42 -33
- package/src/ui/styled/EventsList.tsx +15 -13
- package/src/ui/styled/ForgotPasswordForm.tsx +12 -12
- package/src/ui/styled/FormRenderer.tsx +12 -7
- package/src/ui/styled/InstallBanner.tsx +21 -18
- package/src/ui/styled/InvoiceConfirmation.tsx +13 -5
- package/src/ui/styled/InvoicePayment.tsx +37 -16
- package/src/ui/styled/LeadMagnet.tsx +9 -7
- package/src/ui/styled/Loading.tsx +7 -1
- package/src/ui/styled/LoginForm.tsx +22 -14
- package/src/ui/styled/MembershipCheckout.tsx +32 -13
- package/src/ui/styled/MembershipTiers.tsx +27 -18
- package/src/ui/styled/OfferButton.tsx +33 -13
- package/src/ui/styled/PageActions.tsx +13 -9
- package/src/ui/styled/PaymentLinkConfirmation.tsx +11 -5
- package/src/ui/styled/PaymentLinkPayment.tsx +30 -10
- package/src/ui/styled/PodcastEpisode.tsx +20 -9
- package/src/ui/styled/PodcastList.tsx +9 -3
- package/src/ui/styled/PodcastShow.tsx +29 -7
- package/src/ui/styled/PostsFeed.tsx +10 -8
- package/src/ui/styled/PresaleCode.tsx +8 -11
- package/src/ui/styled/ProductBrowseNav.tsx +21 -11
- package/src/ui/styled/ProductDetail.tsx +58 -28
- package/src/ui/styled/ProductGrid.tsx +17 -4
- package/src/ui/styled/ReactionBar.tsx +4 -2
- package/src/ui/styled/ReplayList.tsx +24 -13
- package/src/ui/styled/ResetPasswordForm.tsx +27 -15
- package/src/ui/styled/ResumeCart.tsx +289 -0
- package/src/ui/styled/ReviewForm.tsx +52 -58
- package/src/ui/styled/SignupForm.tsx +29 -16
- package/src/ui/styled/TicketTransfer.tsx +55 -54
- package/src/ui/styled/UserMenu.tsx +16 -14
- package/src/ui/styled/_tests/ResumeCart.spec.tsx +183 -0
|
@@ -4,6 +4,7 @@ import { CreditCard, MapPin, Package, User } from "lucide-react";
|
|
|
4
4
|
import { useCheckout, CHECKOUT_STAGE_ORDER, type CheckoutStage } from "../headless/checkout/useCheckout";
|
|
5
5
|
import type { SelectedShippingRate } from "../headless/checkout/useCheckout";
|
|
6
6
|
import type { ShippingRate, ShippingGroup } from "../../data/queries/useShipping";
|
|
7
|
+
import { useForgeT } from "../../i18n";
|
|
7
8
|
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
8
9
|
import { readableTextOn } from "../theme/contrast";
|
|
9
10
|
import { useFormatCurrency } from "../format/useFormatCurrency";
|
|
@@ -64,6 +65,8 @@ export function Checkout({
|
|
|
64
65
|
style,
|
|
65
66
|
}: CheckoutProps) {
|
|
66
67
|
const theme = useForgeTheme();
|
|
68
|
+
// `tr`, not `t`: a ticket line below binds `t` as its own map callback param.
|
|
69
|
+
const tr = useForgeT();
|
|
67
70
|
const { formatCurrency } = useFormatCurrency();
|
|
68
71
|
// Store display mode (site config) — drives the pre-quote "incl. tax" caption.
|
|
69
72
|
// Once the payment starts, the authoritative taxQuote from start-payment wins.
|
|
@@ -177,7 +180,9 @@ export function Checkout({
|
|
|
177
180
|
return <Loading fullPage />;
|
|
178
181
|
}
|
|
179
182
|
|
|
180
|
-
|
|
183
|
+
// Tickets count: a cart holding only tickets (buyer skipped the add-ons)
|
|
184
|
+
// is NOT empty, and this page is the only checkout it has.
|
|
185
|
+
if (c.cartItems.length === 0 && c.ticketItems.length === 0) {
|
|
181
186
|
return (
|
|
182
187
|
<div
|
|
183
188
|
className={["w-full max-w-4xl mx-auto p-6", className].filter(Boolean).join(" ")}
|
|
@@ -185,12 +190,12 @@ export function Checkout({
|
|
|
185
190
|
>
|
|
186
191
|
<div className="text-center py-12">
|
|
187
192
|
<h1 className="text-2xl font-bold mb-4" style={{ color: theme.colors.text }}>
|
|
188
|
-
|
|
193
|
+
{tr("forge.checkout.empty_title")}
|
|
189
194
|
</h1>
|
|
190
195
|
<p className="mb-6" style={{ color: `${theme.colors.text}${alpha(0.7)}` }}>
|
|
191
|
-
|
|
196
|
+
{tr("forge.checkout.empty_body")}
|
|
192
197
|
</p>
|
|
193
|
-
<PrimaryButton text="
|
|
198
|
+
<PrimaryButton text={tr("forge.checkout.continue_shopping")} onClick={() => go(storePath)} />
|
|
194
199
|
</div>
|
|
195
200
|
</div>
|
|
196
201
|
);
|
|
@@ -198,14 +203,14 @@ export function Checkout({
|
|
|
198
203
|
|
|
199
204
|
const stages = c.hasPhysicalProduct
|
|
200
205
|
? [
|
|
201
|
-
{ key: "userDetails" as const, label: "
|
|
202
|
-
{ key: "shippingAddress" as const, label: "
|
|
203
|
-
{ key: "shippingRates" as const, label: "
|
|
204
|
-
{ key: "payment" as const, label: "
|
|
206
|
+
{ key: "userDetails" as const, label: tr("forge.checkout.stage_user"), icon: User },
|
|
207
|
+
{ key: "shippingAddress" as const, label: tr("forge.checkout.stage_address"), icon: MapPin },
|
|
208
|
+
{ key: "shippingRates" as const, label: tr("forge.checkout.stage_shipping"), icon: Package },
|
|
209
|
+
{ key: "payment" as const, label: tr("forge.checkout.stage_payment"), icon: CreditCard },
|
|
205
210
|
]
|
|
206
211
|
: [
|
|
207
|
-
{ key: "userDetails" as const, label: "
|
|
208
|
-
{ key: "payment" as const, label: "
|
|
212
|
+
{ key: "userDetails" as const, label: tr("forge.checkout.stage_user"), icon: User },
|
|
213
|
+
{ key: "payment" as const, label: tr("forge.checkout.stage_payment"), icon: CreditCard },
|
|
209
214
|
];
|
|
210
215
|
|
|
211
216
|
const currentIdx = stageIdx(c.currentStage);
|
|
@@ -270,29 +275,35 @@ export function Checkout({
|
|
|
270
275
|
>
|
|
271
276
|
<div className="pb-6" style={{ borderTop: `1px solid ${theme.colors.primary}${alpha(0.2)}` }}>
|
|
272
277
|
<h3 className="text-lg font-semibold mb-4" style={{ color: theme.colors.text }}>
|
|
273
|
-
|
|
278
|
+
{tr("forge.checkout.have_account")}
|
|
274
279
|
</h3>
|
|
275
|
-
<PrimaryButton text="
|
|
280
|
+
<PrimaryButton text={tr("forge.checkout.login")} onClick={handleLogin} fullWidth />
|
|
276
281
|
</div>
|
|
277
282
|
<h2 className="text-2xl font-bold mb-6" style={{ color: theme.colors.text }}>
|
|
278
|
-
|
|
283
|
+
{tr("forge.checkout.or")}
|
|
279
284
|
</h2>
|
|
280
285
|
|
|
281
286
|
<div className="mb-6">
|
|
282
287
|
<h3 className="text-lg font-semibold mb-4" style={{ color: theme.colors.text }}>
|
|
283
|
-
|
|
288
|
+
{tr("forge.checkout.guest_title")}
|
|
284
289
|
</h3>
|
|
285
290
|
<form onSubmit={guestForm.handleSubmit(c.submitGuest)} className="space-y-4">
|
|
286
291
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
287
292
|
<div>
|
|
288
293
|
<label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
|
|
289
|
-
|
|
294
|
+
{tr("forge.checkout.first_name")}
|
|
290
295
|
</label>
|
|
291
296
|
<Controller
|
|
292
297
|
control={guestForm.control}
|
|
293
298
|
name="firstName"
|
|
294
|
-
rules={{ required: "
|
|
295
|
-
render={({ field }) =>
|
|
299
|
+
rules={{ required: tr("forge.checkout.first_name_required") }}
|
|
300
|
+
render={({ field }) => (
|
|
301
|
+
<input
|
|
302
|
+
{...field}
|
|
303
|
+
placeholder={tr("forge.checkout.first_name_placeholder")}
|
|
304
|
+
style={inputStyle}
|
|
305
|
+
/>
|
|
306
|
+
)}
|
|
296
307
|
/>
|
|
297
308
|
{guestForm.formState.errors.firstName && (
|
|
298
309
|
<p className="text-sm text-red-500 mt-1">{guestForm.formState.errors.firstName.message}</p>
|
|
@@ -300,13 +311,19 @@ export function Checkout({
|
|
|
300
311
|
</div>
|
|
301
312
|
<div>
|
|
302
313
|
<label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
|
|
303
|
-
|
|
314
|
+
{tr("forge.checkout.last_name")}
|
|
304
315
|
</label>
|
|
305
316
|
<Controller
|
|
306
317
|
control={guestForm.control}
|
|
307
318
|
name="lastName"
|
|
308
|
-
rules={{ required: "
|
|
309
|
-
render={({ field }) =>
|
|
319
|
+
rules={{ required: tr("forge.checkout.last_name_required") }}
|
|
320
|
+
render={({ field }) => (
|
|
321
|
+
<input
|
|
322
|
+
{...field}
|
|
323
|
+
placeholder={tr("forge.checkout.last_name_placeholder")}
|
|
324
|
+
style={inputStyle}
|
|
325
|
+
/>
|
|
326
|
+
)}
|
|
310
327
|
/>
|
|
311
328
|
{guestForm.formState.errors.lastName && (
|
|
312
329
|
<p className="text-sm text-red-500 mt-1">{guestForm.formState.errors.lastName.message}</p>
|
|
@@ -315,16 +332,26 @@ export function Checkout({
|
|
|
315
332
|
</div>
|
|
316
333
|
<div>
|
|
317
334
|
<label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
|
|
318
|
-
|
|
335
|
+
{tr("forge.checkout.email")}
|
|
319
336
|
</label>
|
|
320
337
|
<Controller
|
|
321
338
|
control={guestForm.control}
|
|
322
339
|
name="email"
|
|
323
340
|
rules={{
|
|
324
|
-
required: "
|
|
325
|
-
pattern: {
|
|
341
|
+
required: tr("forge.checkout.email_required"),
|
|
342
|
+
pattern: {
|
|
343
|
+
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
|
344
|
+
message: tr("forge.checkout.email_invalid"),
|
|
345
|
+
},
|
|
326
346
|
}}
|
|
327
|
-
render={({ field }) =>
|
|
347
|
+
render={({ field }) => (
|
|
348
|
+
<input
|
|
349
|
+
{...field}
|
|
350
|
+
type="email"
|
|
351
|
+
placeholder={tr("forge.checkout.email_placeholder")}
|
|
352
|
+
style={inputStyle}
|
|
353
|
+
/>
|
|
354
|
+
)}
|
|
328
355
|
/>
|
|
329
356
|
{guestForm.formState.errors.email && (
|
|
330
357
|
<p className="text-sm text-red-500 mt-1">{guestForm.formState.errors.email.message}</p>
|
|
@@ -340,10 +367,10 @@ export function Checkout({
|
|
|
340
367
|
style={{ accentColor: theme.colors.primary }}
|
|
341
368
|
/>
|
|
342
369
|
<label htmlFor="joinEmailList" className="text-sm" style={{ color: theme.colors.text }}>
|
|
343
|
-
|
|
370
|
+
{tr("forge.checkout.join_email_list")}
|
|
344
371
|
</label>
|
|
345
372
|
</div>
|
|
346
|
-
<PrimaryButton text="
|
|
373
|
+
<PrimaryButton text={tr("forge.checkout.guest_submit")} type="submit" fullWidth />
|
|
347
374
|
</form>
|
|
348
375
|
</div>
|
|
349
376
|
</div>
|
|
@@ -360,18 +387,20 @@ export function Checkout({
|
|
|
360
387
|
}}
|
|
361
388
|
>
|
|
362
389
|
<h2 className="text-2xl font-bold mb-6" style={{ color: theme.colors.text }}>
|
|
363
|
-
|
|
390
|
+
{tr("forge.checkout.shipping_address_title")}
|
|
364
391
|
</h2>
|
|
365
392
|
<form onSubmit={shippingForm.handleSubmit(c.submitAddress)} className="space-y-4">
|
|
366
393
|
<div>
|
|
367
394
|
<label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
|
|
368
|
-
|
|
395
|
+
{tr("forge.checkout.address1_label")}
|
|
369
396
|
</label>
|
|
370
397
|
<Controller
|
|
371
398
|
control={shippingForm.control}
|
|
372
399
|
name="address1"
|
|
373
|
-
rules={{ required: "
|
|
374
|
-
render={({ field }) =>
|
|
400
|
+
rules={{ required: tr("forge.checkout.address_required") }}
|
|
401
|
+
render={({ field }) => (
|
|
402
|
+
<input {...field} placeholder={tr("forge.checkout.address1_placeholder")} style={inputStyle} />
|
|
403
|
+
)}
|
|
375
404
|
/>
|
|
376
405
|
{shippingForm.formState.errors.address1 && (
|
|
377
406
|
<p className="text-sm text-red-500 mt-1">{shippingForm.formState.errors.address1.message}</p>
|
|
@@ -381,13 +410,15 @@ export function Checkout({
|
|
|
381
410
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
382
411
|
<div>
|
|
383
412
|
<label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
|
|
384
|
-
|
|
413
|
+
{tr("forge.checkout.city_label")}
|
|
385
414
|
</label>
|
|
386
415
|
<Controller
|
|
387
416
|
control={shippingForm.control}
|
|
388
417
|
name="city"
|
|
389
|
-
rules={{ required: "
|
|
390
|
-
render={({ field }) =>
|
|
418
|
+
rules={{ required: tr("forge.checkout.city_required") }}
|
|
419
|
+
render={({ field }) => (
|
|
420
|
+
<input {...field} placeholder={tr("forge.checkout.city_placeholder")} style={inputStyle} />
|
|
421
|
+
)}
|
|
391
422
|
/>
|
|
392
423
|
{shippingForm.formState.errors.city && (
|
|
393
424
|
<p className="text-sm text-red-500 mt-1">{shippingForm.formState.errors.city.message}</p>
|
|
@@ -396,13 +427,15 @@ export function Checkout({
|
|
|
396
427
|
|
|
397
428
|
<div>
|
|
398
429
|
<label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
|
|
399
|
-
|
|
430
|
+
{tr("forge.checkout.zip_label")}
|
|
400
431
|
</label>
|
|
401
432
|
<Controller
|
|
402
433
|
control={shippingForm.control}
|
|
403
434
|
name="zip"
|
|
404
|
-
rules={{ required: "
|
|
405
|
-
render={({ field }) =>
|
|
435
|
+
rules={{ required: tr("forge.checkout.zip_required") }}
|
|
436
|
+
render={({ field }) => (
|
|
437
|
+
<input {...field} placeholder={tr("forge.checkout.zip_placeholder")} style={inputStyle} />
|
|
438
|
+
)}
|
|
406
439
|
/>
|
|
407
440
|
{shippingForm.formState.errors.zip && (
|
|
408
441
|
<p className="text-sm text-red-500 mt-1">{shippingForm.formState.errors.zip.message}</p>
|
|
@@ -413,15 +446,15 @@ export function Checkout({
|
|
|
413
446
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
414
447
|
<div>
|
|
415
448
|
<label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
|
|
416
|
-
|
|
449
|
+
{tr("forge.checkout.country_label")}
|
|
417
450
|
</label>
|
|
418
451
|
<Controller
|
|
419
452
|
control={shippingForm.control}
|
|
420
453
|
name="countryCode"
|
|
421
|
-
rules={{ required: "
|
|
454
|
+
rules={{ required: tr("forge.checkout.country_required") }}
|
|
422
455
|
render={({ field }) => (
|
|
423
456
|
<select {...field} style={inputStyle}>
|
|
424
|
-
<option value="">
|
|
457
|
+
<option value="">{tr("forge.checkout.country_select")}</option>
|
|
425
458
|
{c.countries.map((country) => (
|
|
426
459
|
<option key={country.code} value={country.code}>
|
|
427
460
|
{country.name}
|
|
@@ -438,15 +471,15 @@ export function Checkout({
|
|
|
438
471
|
{selectedCountry?.states && selectedCountry.states.length > 0 && (
|
|
439
472
|
<div>
|
|
440
473
|
<label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
|
|
441
|
-
|
|
474
|
+
{tr("forge.checkout.state_label")}
|
|
442
475
|
</label>
|
|
443
476
|
<Controller
|
|
444
477
|
control={shippingForm.control}
|
|
445
478
|
name="stateCode"
|
|
446
|
-
rules={{ required: "
|
|
479
|
+
rules={{ required: tr("forge.checkout.state_required") }}
|
|
447
480
|
render={({ field }) => (
|
|
448
481
|
<select {...field} style={inputStyle}>
|
|
449
|
-
<option value="">
|
|
482
|
+
<option value="">{tr("forge.checkout.state_select")}</option>
|
|
450
483
|
{selectedCountry.states!.map((state) => (
|
|
451
484
|
<option key={state.code} value={state.code}>
|
|
452
485
|
{state.name}
|
|
@@ -464,13 +497,15 @@ export function Checkout({
|
|
|
464
497
|
|
|
465
498
|
<div>
|
|
466
499
|
<label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
|
|
467
|
-
|
|
500
|
+
{tr("forge.checkout.phone_label")}
|
|
468
501
|
</label>
|
|
469
502
|
<Controller
|
|
470
503
|
control={shippingForm.control}
|
|
471
504
|
name="phone"
|
|
472
|
-
rules={{ required: "
|
|
473
|
-
render={({ field }) =>
|
|
505
|
+
rules={{ required: tr("forge.checkout.phone_required") }}
|
|
506
|
+
render={({ field }) => (
|
|
507
|
+
<input {...field} placeholder={tr("forge.checkout.phone_placeholder")} style={inputStyle} />
|
|
508
|
+
)}
|
|
474
509
|
/>
|
|
475
510
|
{shippingForm.formState.errors.phone && (
|
|
476
511
|
<p className="text-sm text-red-500 mt-1">{shippingForm.formState.errors.phone.message}</p>
|
|
@@ -478,9 +513,18 @@ export function Checkout({
|
|
|
478
513
|
</div>
|
|
479
514
|
|
|
480
515
|
<div className="flex gap-4 pt-4">
|
|
481
|
-
{!c.user &&
|
|
516
|
+
{!c.user && (
|
|
517
|
+
<SecondaryButton
|
|
518
|
+
text={tr("forge.checkout.back")}
|
|
519
|
+
onClick={() => c.setCurrentStage("userDetails")}
|
|
520
|
+
/>
|
|
521
|
+
)}
|
|
482
522
|
<PrimaryButton
|
|
483
|
-
text={
|
|
523
|
+
text={
|
|
524
|
+
c.isFetchingRates
|
|
525
|
+
? tr("forge.checkout.fetching_rates")
|
|
526
|
+
: tr("forge.checkout.continue_to_shipping")
|
|
527
|
+
}
|
|
484
528
|
type="submit"
|
|
485
529
|
fullWidth
|
|
486
530
|
disabled={c.isFetchingRates}
|
|
@@ -501,7 +545,7 @@ export function Checkout({
|
|
|
501
545
|
}}
|
|
502
546
|
>
|
|
503
547
|
<h2 className="text-2xl font-bold mb-6" style={{ color: theme.colors.text }}>
|
|
504
|
-
|
|
548
|
+
{tr("forge.checkout.shipping_options_title")}
|
|
505
549
|
</h2>
|
|
506
550
|
|
|
507
551
|
<ShippingRateSelector
|
|
@@ -512,9 +556,12 @@ export function Checkout({
|
|
|
512
556
|
/>
|
|
513
557
|
|
|
514
558
|
<div className="flex gap-4 pt-6">
|
|
515
|
-
<SecondaryButton
|
|
559
|
+
<SecondaryButton
|
|
560
|
+
text={tr("forge.checkout.back")}
|
|
561
|
+
onClick={() => c.setCurrentStage("shippingAddress")}
|
|
562
|
+
/>
|
|
516
563
|
<PrimaryButton
|
|
517
|
-
text="
|
|
564
|
+
text={tr("forge.checkout.continue_to_payment")}
|
|
518
565
|
onClick={c.confirmRates}
|
|
519
566
|
fullWidth
|
|
520
567
|
disabled={c.shippingGroups.some((g) => g.selectable && !c.selections[g.id])}
|
|
@@ -548,12 +595,17 @@ export function Checkout({
|
|
|
548
595
|
}}
|
|
549
596
|
>
|
|
550
597
|
<h2 className="text-2xl font-bold mb-6" style={{ color: theme.colors.text }}>
|
|
551
|
-
|
|
598
|
+
{tr("forge.checkout.payment_title")}
|
|
552
599
|
</h2>
|
|
553
600
|
<p className="text-sm mb-8" style={{ color: `${theme.colors.text}${alpha(0.7)}` }}>
|
|
554
|
-
|
|
601
|
+
{tr("forge.checkout.free_checkout_body")}
|
|
555
602
|
</p>
|
|
556
|
-
<PrimaryButton
|
|
603
|
+
<PrimaryButton
|
|
604
|
+
text={tr("forge.checkout.complete_checkout")}
|
|
605
|
+
onClick={handleFreeCheckout}
|
|
606
|
+
fullWidth
|
|
607
|
+
disabled={c.isFreeCheckoutLoading}
|
|
608
|
+
/>
|
|
557
609
|
</div>
|
|
558
610
|
)}
|
|
559
611
|
</div>
|
|
@@ -569,13 +621,13 @@ export function Checkout({
|
|
|
569
621
|
}}
|
|
570
622
|
>
|
|
571
623
|
<h3 className="text-xl font-bold mb-4" style={{ color: theme.colors.text }}>
|
|
572
|
-
|
|
624
|
+
{tr("forge.checkout.summary_title")}
|
|
573
625
|
</h3>
|
|
574
626
|
|
|
575
627
|
{/* Customer */}
|
|
576
628
|
<div className="mb-4 pb-4" style={{ borderBottom: `1px solid ${theme.colors.primary}${alpha(0.2)}` }}>
|
|
577
629
|
<h4 className="font-semibold mb-2" style={{ color: theme.colors.text }}>
|
|
578
|
-
|
|
630
|
+
{tr("forge.checkout.summary_customer")}
|
|
579
631
|
</h4>
|
|
580
632
|
{c.user ? (
|
|
581
633
|
<div className="text-sm">
|
|
@@ -593,7 +645,7 @@ export function Checkout({
|
|
|
593
645
|
</div>
|
|
594
646
|
) : (
|
|
595
647
|
<p className="text-sm" style={{ color: `${theme.colors.text}${alpha(0.5)}` }}>
|
|
596
|
-
|
|
648
|
+
{tr("forge.checkout.guest_checkout")}
|
|
597
649
|
</p>
|
|
598
650
|
)}
|
|
599
651
|
</div>
|
|
@@ -602,7 +654,7 @@ export function Checkout({
|
|
|
602
654
|
{c.hasPhysicalProduct && c.shippingData && (
|
|
603
655
|
<div className="mb-4 pb-4" style={{ borderBottom: `1px solid ${theme.colors.primary}${alpha(0.2)}` }}>
|
|
604
656
|
<h4 className="font-semibold mb-2" style={{ color: theme.colors.text }}>
|
|
605
|
-
|
|
657
|
+
{tr("forge.checkout.summary_shipping_address")}
|
|
606
658
|
</h4>
|
|
607
659
|
<div className="text-sm">
|
|
608
660
|
<p style={{ color: theme.colors.text }}>{c.shippingData.address1}</p>
|
|
@@ -619,9 +671,37 @@ export function Checkout({
|
|
|
619
671
|
{/* Cart Items */}
|
|
620
672
|
<div className="mb-4 pb-4" style={{ borderBottom: `1px solid ${theme.colors.primary}${alpha(0.2)}` }}>
|
|
621
673
|
<h4 className="font-semibold mb-2" style={{ color: theme.colors.text }}>
|
|
622
|
-
|
|
674
|
+
{tr("forge.checkout.summary_items")}
|
|
623
675
|
</h4>
|
|
624
676
|
<div className="space-y-2">
|
|
677
|
+
{/* Ticket lines first: they are what the bundle is FOR, and the
|
|
678
|
+
subtotal already includes them, so a summary without them
|
|
679
|
+
reads as a wrong total. */}
|
|
680
|
+
{c.ticketItems.flatMap((t) =>
|
|
681
|
+
Object.entries(t.tickets)
|
|
682
|
+
.filter(([, qty]) => qty > 0)
|
|
683
|
+
.map(([ticketId, qty]) => {
|
|
684
|
+
const meta = t.ticketMeta[ticketId];
|
|
685
|
+
return (
|
|
686
|
+
<div key={`${t.eventId}-${ticketId}`} className="flex justify-between text-sm" data-testid="summary-ticket-line">
|
|
687
|
+
<div className="flex-1">
|
|
688
|
+
<p className="font-medium" style={{ color: theme.colors.text }}>
|
|
689
|
+
{meta?.title ?? tr("forge.checkout.ticket_fallback")}
|
|
690
|
+
</p>
|
|
691
|
+
<p className="text-xs" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
|
|
692
|
+
{t.eventTitle}
|
|
693
|
+
</p>
|
|
694
|
+
<p className="text-xs" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
|
|
695
|
+
{tr("forge.checkout.qty", { count: qty })}
|
|
696
|
+
</p>
|
|
697
|
+
</div>
|
|
698
|
+
<p className="font-medium" style={{ color: theme.colors.text }}>
|
|
699
|
+
{formatCurrency((meta?.price ?? 0) * qty)}
|
|
700
|
+
</p>
|
|
701
|
+
</div>
|
|
702
|
+
);
|
|
703
|
+
}),
|
|
704
|
+
)}
|
|
625
705
|
{c.cartItems.map((item) => (
|
|
626
706
|
<div
|
|
627
707
|
key={`${item.productId}-${item.productVariantId}-${item.isGift}-${item.recipientEmail}`}
|
|
@@ -637,11 +717,11 @@ export function Checkout({
|
|
|
637
717
|
<CartLineOptions item={item} accent={theme.colors.primary} size="small" />
|
|
638
718
|
{item.isGift && (
|
|
639
719
|
<p className="text-xs" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
|
|
640
|
-
|
|
720
|
+
{tr("forge.checkout.gift_for", { recipient_name: item.recipientName ?? "" })}
|
|
641
721
|
</p>
|
|
642
722
|
)}
|
|
643
723
|
<p className="text-xs" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
|
|
644
|
-
|
|
724
|
+
{tr("forge.checkout.qty", { count: item.quantity })}
|
|
645
725
|
</p>
|
|
646
726
|
</div>
|
|
647
727
|
<p className="font-medium" style={{ color: theme.colors.text }}>
|
|
@@ -668,25 +748,25 @@ export function Checkout({
|
|
|
668
748
|
style={{ color: theme.colors.primary }}
|
|
669
749
|
onClick={() => c.setShowCouponInput(true)}
|
|
670
750
|
>
|
|
671
|
-
|
|
751
|
+
{tr("forge.checkout.coupon_prompt")}
|
|
672
752
|
</button>
|
|
673
753
|
)}
|
|
674
754
|
{c.showCouponInput && !c.appliedCoupon && c.isCouponEditable && (
|
|
675
755
|
<div className="flex gap-2 items-end">
|
|
676
756
|
<div className="flex-1">
|
|
677
757
|
<p className="text-sm mb-1" style={{ color: theme.colors.text }}>
|
|
678
|
-
|
|
758
|
+
{tr("forge.checkout.coupon_label")}
|
|
679
759
|
</p>
|
|
680
760
|
<input
|
|
681
|
-
aria-label="
|
|
682
|
-
placeholder="
|
|
761
|
+
aria-label={tr("forge.checkout.aria_coupon_code")}
|
|
762
|
+
placeholder={tr("forge.checkout.coupon_placeholder")}
|
|
683
763
|
style={inputStyle}
|
|
684
764
|
value={c.couponCode}
|
|
685
765
|
onChange={(e) => c.setCouponCode(e.target.value.toUpperCase())}
|
|
686
766
|
/>
|
|
687
767
|
</div>
|
|
688
768
|
<PrimaryButton
|
|
689
|
-
text={c.isApplyingCoupon ? "..." : "
|
|
769
|
+
text={c.isApplyingCoupon ? "..." : tr("forge.checkout.coupon_apply")}
|
|
690
770
|
onClick={c.applyCoupon}
|
|
691
771
|
disabled={c.isApplyingCoupon || !c.couponCode.trim()}
|
|
692
772
|
/>
|
|
@@ -696,7 +776,7 @@ export function Checkout({
|
|
|
696
776
|
code is priced live, so promising "at payment" would be wrong. */}
|
|
697
777
|
{c.isBundle && !c.checkoutId && c.showCouponInput && !c.appliedCoupon && c.isCouponEditable && (
|
|
698
778
|
<p className="text-xs mt-1" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
|
|
699
|
-
|
|
779
|
+
{tr("forge.checkout.coupon_deferred_note")}
|
|
700
780
|
</p>
|
|
701
781
|
)}
|
|
702
782
|
{c.couponError && <p className="text-sm text-red-500 mt-1">{c.couponError}</p>}
|
|
@@ -707,19 +787,25 @@ export function Checkout({
|
|
|
707
787
|
>
|
|
708
788
|
<div>
|
|
709
789
|
<p className="text-sm font-semibold" style={{ color: theme.colors.primary }}>
|
|
710
|
-
|
|
790
|
+
{tr("forge.checkout.coupon_applied", { code: c.appliedCoupon.code })}
|
|
711
791
|
</p>
|
|
712
792
|
{/* A bundle reports the cash that came off and the code it
|
|
713
793
|
came off under, but not the rate — so say the cash,
|
|
714
794
|
never a made-up "0% off". */}
|
|
715
795
|
<p className="text-xs" style={{ color: theme.colors.text }}>
|
|
716
796
|
{c.appliedCoupon.discountType === "percentage"
|
|
717
|
-
?
|
|
797
|
+
? // `String(...)` keeps a null rate rendering exactly as the old template
|
|
798
|
+
// literal did, rather than quietly turning it into a made-up "0".
|
|
799
|
+
tr("forge.checkout.coupon_percent_off", { percent: String(c.appliedCoupon.discountValue) })
|
|
718
800
|
: c.appliedCoupon.discountValue != null
|
|
719
|
-
?
|
|
801
|
+
? tr("forge.checkout.coupon_amount_off", {
|
|
802
|
+
amount: formatCurrency(c.appliedCoupon.discountValue),
|
|
803
|
+
})
|
|
720
804
|
: c.appliedCoupon.discountAmount != null
|
|
721
|
-
?
|
|
722
|
-
|
|
805
|
+
? tr("forge.checkout.coupon_amount_off", {
|
|
806
|
+
amount: formatCurrency(c.appliedCoupon.discountAmount),
|
|
807
|
+
})
|
|
808
|
+
: tr("forge.checkout.coupon_applied_at_payment")}
|
|
723
809
|
</p>
|
|
724
810
|
</div>
|
|
725
811
|
{c.isCouponEditable && (
|
|
@@ -730,7 +816,7 @@ export function Checkout({
|
|
|
730
816
|
onClick={c.removeCoupon}
|
|
731
817
|
disabled={c.isApplyingCoupon}
|
|
732
818
|
>
|
|
733
|
-
|
|
819
|
+
{tr("forge.checkout.coupon_remove")}
|
|
734
820
|
</button>
|
|
735
821
|
)}
|
|
736
822
|
</div>
|
|
@@ -741,26 +827,35 @@ export function Checkout({
|
|
|
741
827
|
{/* Totals */}
|
|
742
828
|
<div className="space-y-2">
|
|
743
829
|
<div className="flex justify-between">
|
|
744
|
-
<span style={{ color: theme.colors.text }}>
|
|
830
|
+
<span style={{ color: theme.colors.text }}>{tr("forge.checkout.subtotal")}</span>
|
|
745
831
|
<span style={{ color: theme.colors.text }}>{formatCurrency(c.subTotal)}</span>
|
|
746
832
|
</div>
|
|
747
833
|
{c.discountAmount > 0 && (
|
|
748
834
|
<div className="flex justify-between">
|
|
749
|
-
<span style={{ color: theme.colors.text }}>
|
|
835
|
+
<span style={{ color: theme.colors.text }}>{tr("forge.checkout.discount")}</span>
|
|
750
836
|
<span style={{ color: theme.colors.primary }}>-{formatCurrency(c.discountAmount)}</span>
|
|
751
837
|
</div>
|
|
752
838
|
)}
|
|
753
839
|
{c.shippingCost > 0 && (
|
|
754
840
|
<div className="flex justify-between">
|
|
755
|
-
<span style={{ color: theme.colors.text }}>
|
|
841
|
+
<span style={{ color: theme.colors.text }}>{tr("forge.checkout.shipping")}</span>
|
|
756
842
|
<span style={{ color: theme.colors.text }}>{formatCurrency(c.shippingCost)}</span>
|
|
757
843
|
</div>
|
|
758
844
|
)}
|
|
845
|
+
{/* The ticket booking fee, named BEFORE the buyer commits. It is
|
|
846
|
+
charged either way; omitting the line here left a total that
|
|
847
|
+
jumped past the subtotal with no explanation. */}
|
|
848
|
+
{c.bookingFeeCost > 0 && (
|
|
849
|
+
<div className="flex justify-between" data-testid="summary-booking-fee">
|
|
850
|
+
<span style={{ color: theme.colors.text }}>{tr("forge.checkout.booking_fee")}</span>
|
|
851
|
+
<span style={{ color: theme.colors.text }}>{formatCurrency(c.bookingFeeCost)}</span>
|
|
852
|
+
</div>
|
|
853
|
+
)}
|
|
759
854
|
{/* Exclusive mode: the authoritative tax line, once the checkout
|
|
760
855
|
quote is known (added on top of the total). */}
|
|
761
856
|
{taxSummary?.mode === "exclusive" && (
|
|
762
857
|
<div className="flex justify-between">
|
|
763
|
-
<span style={{ color: theme.colors.text }}>
|
|
858
|
+
<span style={{ color: theme.colors.text }}>{tr("forge.checkout.tax")}</span>
|
|
764
859
|
<span style={{ color: theme.colors.text }}>{taxSummary.formattedTax}</span>
|
|
765
860
|
</div>
|
|
766
861
|
)}
|
|
@@ -768,11 +863,11 @@ export function Checkout({
|
|
|
768
863
|
className="flex justify-between font-bold text-lg pt-2"
|
|
769
864
|
style={{ borderTop: `1px solid ${theme.colors.primary}${alpha(0.2)}`, color: theme.colors.primary }}
|
|
770
865
|
>
|
|
771
|
-
<span>
|
|
866
|
+
<span>{tr("forge.checkout.total")}</span>
|
|
772
867
|
<span>
|
|
773
868
|
{c.chargedTotal
|
|
774
869
|
? formatCurrency(c.chargedTotal.amount, c.chargedTotal.currency)
|
|
775
|
-
: formatCurrency(c.subTotal + c.shippingCost)}
|
|
870
|
+
: formatCurrency(c.subTotal + c.shippingCost + c.bookingFeeCost)}
|
|
776
871
|
</span>
|
|
777
872
|
</div>
|
|
778
873
|
{/* Inclusive mode: the total already contains tax — break out the
|
|
@@ -785,7 +880,7 @@ export function Checkout({
|
|
|
785
880
|
!taxSummary &&
|
|
786
881
|
pricesIncludeTax && (
|
|
787
882
|
<p className="text-xs text-right" style={{ color: `${theme.colors.text}${alpha(0.6)}`, margin: 0 }}>
|
|
788
|
-
|
|
883
|
+
{tr("forge.checkout.incl_tax")}
|
|
789
884
|
</p>
|
|
790
885
|
)
|
|
791
886
|
)}
|
|
@@ -818,6 +913,7 @@ function PaymentSection({
|
|
|
818
913
|
onBack?: () => void;
|
|
819
914
|
}) {
|
|
820
915
|
const theme = useForgeTheme();
|
|
916
|
+
const tr = useForgeT();
|
|
821
917
|
const renderPay = usePaymentRenderer();
|
|
822
918
|
const { paymentSecret, chargedAmount, chargedCurrency, returnUrl, isPaystack, isCreatingOrder, startError } = checkout;
|
|
823
919
|
const { canOpenPaystack, openPaystackCheckout } = checkout;
|
|
@@ -839,7 +935,7 @@ function PaymentSection({
|
|
|
839
935
|
borderRadius: `${theme.cornerRadius}px`,
|
|
840
936
|
}}
|
|
841
937
|
>
|
|
842
|
-
|
|
938
|
+
{tr("forge.checkout.back")}
|
|
843
939
|
</button>
|
|
844
940
|
) : null;
|
|
845
941
|
|
|
@@ -855,10 +951,10 @@ function PaymentSection({
|
|
|
855
951
|
}}
|
|
856
952
|
>
|
|
857
953
|
<h2 className="text-2xl font-bold mb-2" style={{ color: theme.colors.text }}>
|
|
858
|
-
|
|
954
|
+
{tr("forge.checkout.payment_heading")}
|
|
859
955
|
</h2>
|
|
860
956
|
<p className="text-lg mb-4" style={{ color: theme.colors.text }}>
|
|
861
|
-
|
|
957
|
+
{tr("forge.checkout.payment_not_configured")}
|
|
862
958
|
</p>
|
|
863
959
|
{backButton}
|
|
864
960
|
</div>
|
|
@@ -875,7 +971,7 @@ function PaymentSection({
|
|
|
875
971
|
}}
|
|
876
972
|
>
|
|
877
973
|
<h2 className="text-2xl font-bold mb-6" style={{ color: theme.colors.text }}>
|
|
878
|
-
|
|
974
|
+
{tr("forge.checkout.payment_title")}
|
|
879
975
|
</h2>
|
|
880
976
|
|
|
881
977
|
{startError && (
|
|
@@ -893,13 +989,15 @@ function PaymentSection({
|
|
|
893
989
|
expiredMessage={holdExpiredError || undefined}
|
|
894
990
|
onRetry={retryHold}
|
|
895
991
|
isRetrying={isRetryingHold}
|
|
896
|
-
secondaryAction={onBack ? { label: "
|
|
992
|
+
secondaryAction={onBack ? { label: tr("forge.checkout.hold_change_order"), onClick: onBack } : undefined}
|
|
897
993
|
/>
|
|
898
994
|
|
|
899
995
|
{/* Suppressed once the server has refused: there is no intent coming, so a
|
|
900
996
|
"Preparing payment…" spinner would sit there forever under a notice
|
|
901
997
|
that has already explained what happened. */}
|
|
902
|
-
{isCreatingOrder && !hasCharge && !isHoldExpired &&
|
|
998
|
+
{isCreatingOrder && !hasCharge && !isHoldExpired && (
|
|
999
|
+
<Loading label={tr("forge.checkout.preparing_payment")} size={22} />
|
|
1000
|
+
)}
|
|
903
1001
|
|
|
904
1002
|
{isPaystack
|
|
905
1003
|
? canOpenPaystack && (
|
|
@@ -946,6 +1044,7 @@ function ShippingRateSelector({
|
|
|
946
1044
|
formatCurrency: (amount: number, currencyCode?: string) => string;
|
|
947
1045
|
}) {
|
|
948
1046
|
const theme = useForgeTheme();
|
|
1047
|
+
const tr = useForgeT();
|
|
949
1048
|
if (groups.length === 0) return null;
|
|
950
1049
|
|
|
951
1050
|
return (
|
|
@@ -972,7 +1071,7 @@ function ShippingRateSelector({
|
|
|
972
1071
|
</span>
|
|
973
1072
|
{group.rates[0].estimatedDays != null && (
|
|
974
1073
|
<span className="text-xs ml-2" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
|
|
975
|
-
({group.rates[0].estimatedDays}
|
|
1074
|
+
{tr("forge.checkout.rate_days", { days: group.rates[0].estimatedDays })}
|
|
976
1075
|
</span>
|
|
977
1076
|
)}
|
|
978
1077
|
</div>
|
|
@@ -985,7 +1084,7 @@ function ShippingRateSelector({
|
|
|
985
1084
|
<div className="space-y-2">
|
|
986
1085
|
{group.rates.length === 0 && (
|
|
987
1086
|
<p className="text-sm" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
|
|
988
|
-
|
|
1087
|
+
{tr("forge.checkout.no_shipping_options")}
|
|
989
1088
|
</p>
|
|
990
1089
|
)}
|
|
991
1090
|
{group.rates.map((rate) => {
|
|
@@ -1014,7 +1113,9 @@ function ShippingRateSelector({
|
|
|
1014
1113
|
</span>
|
|
1015
1114
|
{rate.estimatedDays && (
|
|
1016
1115
|
<p className="text-xs" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
|
|
1017
|
-
|
|
1116
|
+
{rate.estimatedDays === 1
|
|
1117
|
+
? tr("forge.checkout.rate_estimated_day", { days: rate.estimatedDays })
|
|
1118
|
+
: tr("forge.checkout.rate_estimated_days", { days: rate.estimatedDays })}
|
|
1018
1119
|
</p>
|
|
1019
1120
|
)}
|
|
1020
1121
|
</div>
|