@tribe-nest/forge 3.26.0 → 3.31.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 +6 -3
- package/src/_tests/publishedResolvability.spec.ts +184 -0
- package/src/_tests/specsRunWorkspaceSource.spec.ts +116 -0
- package/src/_tests/workspaceAliases.ts +40 -0
- package/src/client/_tests/localeHeader.spec.tsx +98 -0
- package/src/client/activeLocale.ts +34 -0
- package/src/client/createForgeClient.ts +13 -0
- package/src/contexts/CartContext.tsx +76 -0
- package/src/contexts/PublicAuthContext.tsx +34 -5
- package/src/contexts/_tests/CartContext.spec.tsx +210 -0
- package/src/contexts/_tests/PublicAuthRefetch.spec.tsx +147 -0
- package/src/data/queries/useBroadcasts.ts +151 -0
- package/src/data/queries/useCheckouts.ts +83 -0
- package/src/data/queries/useMyBookings.ts +9 -1
- 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 +1019 -0
- package/src/i18n/en.json +1019 -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/format/_tests/membershipPwyw.spec.ts +185 -0
- package/src/ui/format/_tests/pwyw.spec.ts +65 -8
- package/src/ui/format/membershipPwyw.ts +164 -0
- package/src/ui/format/pwyw.ts +37 -0
- package/src/ui/headless/broadcast/_tests/broadcastState.spec.ts +235 -0
- package/src/ui/headless/broadcast/broadcastState.ts +158 -0
- package/src/ui/headless/broadcast/useBroadcastWatch.ts +174 -21
- 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 +9 -2
- package/src/ui/headless/event/useEventCheckout.ts +8 -13
- package/src/ui/headless/index.ts +32 -0
- package/src/ui/headless/membership/useMembershipCheckout.ts +160 -32
- package/src/ui/index.ts +38 -0
- package/src/ui/media/CallHelpHint.tsx +87 -0
- package/src/ui/media/CallStage.tsx +542 -0
- package/src/ui/media/_tests/CallStage.spec.tsx +685 -0
- package/src/ui/media/_tests/bookingSession.spec.tsx +179 -0
- package/src/ui/media/_tests/callState.spec.ts +452 -0
- package/src/ui/media/_tests/fakeNode.ts +178 -0
- package/src/ui/media/bookingSession.tsx +194 -0
- package/src/ui/media/callState.ts +341 -0
- package/src/ui/media/index.ts +135 -0
- package/src/ui/shell/TribeNestApp.tsx +36 -27
- package/src/ui/styled/AccountDashboard.tsx +289 -96
- 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/BroadcastWatch.tsx +107 -0
- package/src/ui/styled/BundleConfirmation.tsx +32 -23
- 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 +151 -89
- 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 +232 -108
- package/src/ui/styled/EventWaitlist.tsx +42 -33
- package/src/ui/styled/EventsList.tsx +15 -13
- package/src/ui/styled/ForgotPasswordForm.tsx +17 -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/LiveBroadcastList.tsx +171 -0
- package/src/ui/styled/Loading.tsx +7 -1
- package/src/ui/styled/LoginForm.tsx +32 -14
- package/src/ui/styled/MembershipCheckout.tsx +341 -49
- package/src/ui/styled/MembershipTiers.tsx +34 -18
- package/src/ui/styled/OfferButton.tsx +33 -13
- 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/ReactionBar.tsx +4 -2
- package/src/ui/styled/ReplayList.tsx +24 -13
- package/src/ui/styled/ResetPasswordForm.tsx +32 -15
- package/src/ui/styled/ResumeCart.tsx +289 -0
- package/src/ui/styled/ReviewForm.tsx +52 -58
- package/src/ui/styled/SignupForm.tsx +34 -16
- package/src/ui/styled/TicketTransfer.tsx +55 -54
- package/src/ui/styled/UserMenu.tsx +16 -14
- package/src/ui/styled/_tests/AccountDashboardCommunity.spec.tsx +134 -0
- package/src/ui/styled/_tests/BroadcastPassValidation.spec.tsx +125 -0
- package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +364 -0
- package/src/ui/styled/_tests/ResumeCart.spec.tsx +183 -0
- package/src/ui/styled/broadcast/BroadcastPassValidation.tsx +187 -0
- package/src/ui/styled/broadcast/BroadcastPlayer.tsx +536 -0
- package/src/ui/styled/broadcast/BroadcastTicketPurchase.tsx +74 -0
- package/src/ui/styled/broadcast/EndedBroadcast.tsx +103 -0
- package/src/ui/styled/community/CommunityComposer.tsx +182 -3
- package/src/ui/styled/community/CommunityFeed.tsx +36 -51
- package/src/ui/styled/community/CommunityPostDetail.tsx +16 -2
- package/src/ui/styled/community/_tests/CommunityComposer.spec.tsx +281 -0
- package/src/ui/styled/community/_tests/CommunityPostDetail.spec.tsx +175 -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.
|
|
@@ -187,12 +190,12 @@ export function Checkout({
|
|
|
187
190
|
>
|
|
188
191
|
<div className="text-center py-12">
|
|
189
192
|
<h1 className="text-2xl font-bold mb-4" style={{ color: theme.colors.text }}>
|
|
190
|
-
|
|
193
|
+
{tr("forge.checkout.empty_title")}
|
|
191
194
|
</h1>
|
|
192
195
|
<p className="mb-6" style={{ color: `${theme.colors.text}${alpha(0.7)}` }}>
|
|
193
|
-
|
|
196
|
+
{tr("forge.checkout.empty_body")}
|
|
194
197
|
</p>
|
|
195
|
-
<PrimaryButton text="
|
|
198
|
+
<PrimaryButton text={tr("forge.checkout.continue_shopping")} onClick={() => go(storePath)} />
|
|
196
199
|
</div>
|
|
197
200
|
</div>
|
|
198
201
|
);
|
|
@@ -200,14 +203,14 @@ export function Checkout({
|
|
|
200
203
|
|
|
201
204
|
const stages = c.hasPhysicalProduct
|
|
202
205
|
? [
|
|
203
|
-
{ key: "userDetails" as const, label: "
|
|
204
|
-
{ key: "shippingAddress" as const, label: "
|
|
205
|
-
{ key: "shippingRates" as const, label: "
|
|
206
|
-
{ 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 },
|
|
207
210
|
]
|
|
208
211
|
: [
|
|
209
|
-
{ key: "userDetails" as const, label: "
|
|
210
|
-
{ 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 },
|
|
211
214
|
];
|
|
212
215
|
|
|
213
216
|
const currentIdx = stageIdx(c.currentStage);
|
|
@@ -272,29 +275,35 @@ export function Checkout({
|
|
|
272
275
|
>
|
|
273
276
|
<div className="pb-6" style={{ borderTop: `1px solid ${theme.colors.primary}${alpha(0.2)}` }}>
|
|
274
277
|
<h3 className="text-lg font-semibold mb-4" style={{ color: theme.colors.text }}>
|
|
275
|
-
|
|
278
|
+
{tr("forge.checkout.have_account")}
|
|
276
279
|
</h3>
|
|
277
|
-
<PrimaryButton text="
|
|
280
|
+
<PrimaryButton text={tr("forge.checkout.login")} onClick={handleLogin} fullWidth />
|
|
278
281
|
</div>
|
|
279
282
|
<h2 className="text-2xl font-bold mb-6" style={{ color: theme.colors.text }}>
|
|
280
|
-
|
|
283
|
+
{tr("forge.checkout.or")}
|
|
281
284
|
</h2>
|
|
282
285
|
|
|
283
286
|
<div className="mb-6">
|
|
284
287
|
<h3 className="text-lg font-semibold mb-4" style={{ color: theme.colors.text }}>
|
|
285
|
-
|
|
288
|
+
{tr("forge.checkout.guest_title")}
|
|
286
289
|
</h3>
|
|
287
290
|
<form onSubmit={guestForm.handleSubmit(c.submitGuest)} className="space-y-4">
|
|
288
291
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
289
292
|
<div>
|
|
290
293
|
<label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
|
|
291
|
-
|
|
294
|
+
{tr("forge.checkout.first_name")}
|
|
292
295
|
</label>
|
|
293
296
|
<Controller
|
|
294
297
|
control={guestForm.control}
|
|
295
298
|
name="firstName"
|
|
296
|
-
rules={{ required: "
|
|
297
|
-
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
|
+
)}
|
|
298
307
|
/>
|
|
299
308
|
{guestForm.formState.errors.firstName && (
|
|
300
309
|
<p className="text-sm text-red-500 mt-1">{guestForm.formState.errors.firstName.message}</p>
|
|
@@ -302,13 +311,19 @@ export function Checkout({
|
|
|
302
311
|
</div>
|
|
303
312
|
<div>
|
|
304
313
|
<label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
|
|
305
|
-
|
|
314
|
+
{tr("forge.checkout.last_name")}
|
|
306
315
|
</label>
|
|
307
316
|
<Controller
|
|
308
317
|
control={guestForm.control}
|
|
309
318
|
name="lastName"
|
|
310
|
-
rules={{ required: "
|
|
311
|
-
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
|
+
)}
|
|
312
327
|
/>
|
|
313
328
|
{guestForm.formState.errors.lastName && (
|
|
314
329
|
<p className="text-sm text-red-500 mt-1">{guestForm.formState.errors.lastName.message}</p>
|
|
@@ -317,16 +332,26 @@ export function Checkout({
|
|
|
317
332
|
</div>
|
|
318
333
|
<div>
|
|
319
334
|
<label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
|
|
320
|
-
|
|
335
|
+
{tr("forge.checkout.email")}
|
|
321
336
|
</label>
|
|
322
337
|
<Controller
|
|
323
338
|
control={guestForm.control}
|
|
324
339
|
name="email"
|
|
325
340
|
rules={{
|
|
326
|
-
required: "
|
|
327
|
-
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
|
+
},
|
|
328
346
|
}}
|
|
329
|
-
render={({ field }) =>
|
|
347
|
+
render={({ field }) => (
|
|
348
|
+
<input
|
|
349
|
+
{...field}
|
|
350
|
+
type="email"
|
|
351
|
+
placeholder={tr("forge.checkout.email_placeholder")}
|
|
352
|
+
style={inputStyle}
|
|
353
|
+
/>
|
|
354
|
+
)}
|
|
330
355
|
/>
|
|
331
356
|
{guestForm.formState.errors.email && (
|
|
332
357
|
<p className="text-sm text-red-500 mt-1">{guestForm.formState.errors.email.message}</p>
|
|
@@ -342,10 +367,10 @@ export function Checkout({
|
|
|
342
367
|
style={{ accentColor: theme.colors.primary }}
|
|
343
368
|
/>
|
|
344
369
|
<label htmlFor="joinEmailList" className="text-sm" style={{ color: theme.colors.text }}>
|
|
345
|
-
|
|
370
|
+
{tr("forge.checkout.join_email_list")}
|
|
346
371
|
</label>
|
|
347
372
|
</div>
|
|
348
|
-
<PrimaryButton text="
|
|
373
|
+
<PrimaryButton text={tr("forge.checkout.guest_submit")} type="submit" fullWidth />
|
|
349
374
|
</form>
|
|
350
375
|
</div>
|
|
351
376
|
</div>
|
|
@@ -362,18 +387,20 @@ export function Checkout({
|
|
|
362
387
|
}}
|
|
363
388
|
>
|
|
364
389
|
<h2 className="text-2xl font-bold mb-6" style={{ color: theme.colors.text }}>
|
|
365
|
-
|
|
390
|
+
{tr("forge.checkout.shipping_address_title")}
|
|
366
391
|
</h2>
|
|
367
392
|
<form onSubmit={shippingForm.handleSubmit(c.submitAddress)} className="space-y-4">
|
|
368
393
|
<div>
|
|
369
394
|
<label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
|
|
370
|
-
|
|
395
|
+
{tr("forge.checkout.address1_label")}
|
|
371
396
|
</label>
|
|
372
397
|
<Controller
|
|
373
398
|
control={shippingForm.control}
|
|
374
399
|
name="address1"
|
|
375
|
-
rules={{ required: "
|
|
376
|
-
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
|
+
)}
|
|
377
404
|
/>
|
|
378
405
|
{shippingForm.formState.errors.address1 && (
|
|
379
406
|
<p className="text-sm text-red-500 mt-1">{shippingForm.formState.errors.address1.message}</p>
|
|
@@ -383,13 +410,15 @@ export function Checkout({
|
|
|
383
410
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
384
411
|
<div>
|
|
385
412
|
<label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
|
|
386
|
-
|
|
413
|
+
{tr("forge.checkout.city_label")}
|
|
387
414
|
</label>
|
|
388
415
|
<Controller
|
|
389
416
|
control={shippingForm.control}
|
|
390
417
|
name="city"
|
|
391
|
-
rules={{ required: "
|
|
392
|
-
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
|
+
)}
|
|
393
422
|
/>
|
|
394
423
|
{shippingForm.formState.errors.city && (
|
|
395
424
|
<p className="text-sm text-red-500 mt-1">{shippingForm.formState.errors.city.message}</p>
|
|
@@ -398,13 +427,15 @@ export function Checkout({
|
|
|
398
427
|
|
|
399
428
|
<div>
|
|
400
429
|
<label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
|
|
401
|
-
|
|
430
|
+
{tr("forge.checkout.zip_label")}
|
|
402
431
|
</label>
|
|
403
432
|
<Controller
|
|
404
433
|
control={shippingForm.control}
|
|
405
434
|
name="zip"
|
|
406
|
-
rules={{ required: "
|
|
407
|
-
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
|
+
)}
|
|
408
439
|
/>
|
|
409
440
|
{shippingForm.formState.errors.zip && (
|
|
410
441
|
<p className="text-sm text-red-500 mt-1">{shippingForm.formState.errors.zip.message}</p>
|
|
@@ -415,15 +446,15 @@ export function Checkout({
|
|
|
415
446
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
416
447
|
<div>
|
|
417
448
|
<label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
|
|
418
|
-
|
|
449
|
+
{tr("forge.checkout.country_label")}
|
|
419
450
|
</label>
|
|
420
451
|
<Controller
|
|
421
452
|
control={shippingForm.control}
|
|
422
453
|
name="countryCode"
|
|
423
|
-
rules={{ required: "
|
|
454
|
+
rules={{ required: tr("forge.checkout.country_required") }}
|
|
424
455
|
render={({ field }) => (
|
|
425
456
|
<select {...field} style={inputStyle}>
|
|
426
|
-
<option value="">
|
|
457
|
+
<option value="">{tr("forge.checkout.country_select")}</option>
|
|
427
458
|
{c.countries.map((country) => (
|
|
428
459
|
<option key={country.code} value={country.code}>
|
|
429
460
|
{country.name}
|
|
@@ -440,15 +471,15 @@ export function Checkout({
|
|
|
440
471
|
{selectedCountry?.states && selectedCountry.states.length > 0 && (
|
|
441
472
|
<div>
|
|
442
473
|
<label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
|
|
443
|
-
|
|
474
|
+
{tr("forge.checkout.state_label")}
|
|
444
475
|
</label>
|
|
445
476
|
<Controller
|
|
446
477
|
control={shippingForm.control}
|
|
447
478
|
name="stateCode"
|
|
448
|
-
rules={{ required: "
|
|
479
|
+
rules={{ required: tr("forge.checkout.state_required") }}
|
|
449
480
|
render={({ field }) => (
|
|
450
481
|
<select {...field} style={inputStyle}>
|
|
451
|
-
<option value="">
|
|
482
|
+
<option value="">{tr("forge.checkout.state_select")}</option>
|
|
452
483
|
{selectedCountry.states!.map((state) => (
|
|
453
484
|
<option key={state.code} value={state.code}>
|
|
454
485
|
{state.name}
|
|
@@ -466,13 +497,15 @@ export function Checkout({
|
|
|
466
497
|
|
|
467
498
|
<div>
|
|
468
499
|
<label className="block text-sm font-medium mb-2" style={{ color: theme.colors.text }}>
|
|
469
|
-
|
|
500
|
+
{tr("forge.checkout.phone_label")}
|
|
470
501
|
</label>
|
|
471
502
|
<Controller
|
|
472
503
|
control={shippingForm.control}
|
|
473
504
|
name="phone"
|
|
474
|
-
rules={{ required: "
|
|
475
|
-
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
|
+
)}
|
|
476
509
|
/>
|
|
477
510
|
{shippingForm.formState.errors.phone && (
|
|
478
511
|
<p className="text-sm text-red-500 mt-1">{shippingForm.formState.errors.phone.message}</p>
|
|
@@ -480,9 +513,18 @@ export function Checkout({
|
|
|
480
513
|
</div>
|
|
481
514
|
|
|
482
515
|
<div className="flex gap-4 pt-4">
|
|
483
|
-
{!c.user &&
|
|
516
|
+
{!c.user && (
|
|
517
|
+
<SecondaryButton
|
|
518
|
+
text={tr("forge.checkout.back")}
|
|
519
|
+
onClick={() => c.setCurrentStage("userDetails")}
|
|
520
|
+
/>
|
|
521
|
+
)}
|
|
484
522
|
<PrimaryButton
|
|
485
|
-
text={
|
|
523
|
+
text={
|
|
524
|
+
c.isFetchingRates
|
|
525
|
+
? tr("forge.checkout.fetching_rates")
|
|
526
|
+
: tr("forge.checkout.continue_to_shipping")
|
|
527
|
+
}
|
|
486
528
|
type="submit"
|
|
487
529
|
fullWidth
|
|
488
530
|
disabled={c.isFetchingRates}
|
|
@@ -503,7 +545,7 @@ export function Checkout({
|
|
|
503
545
|
}}
|
|
504
546
|
>
|
|
505
547
|
<h2 className="text-2xl font-bold mb-6" style={{ color: theme.colors.text }}>
|
|
506
|
-
|
|
548
|
+
{tr("forge.checkout.shipping_options_title")}
|
|
507
549
|
</h2>
|
|
508
550
|
|
|
509
551
|
<ShippingRateSelector
|
|
@@ -514,9 +556,12 @@ export function Checkout({
|
|
|
514
556
|
/>
|
|
515
557
|
|
|
516
558
|
<div className="flex gap-4 pt-6">
|
|
517
|
-
<SecondaryButton
|
|
559
|
+
<SecondaryButton
|
|
560
|
+
text={tr("forge.checkout.back")}
|
|
561
|
+
onClick={() => c.setCurrentStage("shippingAddress")}
|
|
562
|
+
/>
|
|
518
563
|
<PrimaryButton
|
|
519
|
-
text="
|
|
564
|
+
text={tr("forge.checkout.continue_to_payment")}
|
|
520
565
|
onClick={c.confirmRates}
|
|
521
566
|
fullWidth
|
|
522
567
|
disabled={c.shippingGroups.some((g) => g.selectable && !c.selections[g.id])}
|
|
@@ -550,12 +595,17 @@ export function Checkout({
|
|
|
550
595
|
}}
|
|
551
596
|
>
|
|
552
597
|
<h2 className="text-2xl font-bold mb-6" style={{ color: theme.colors.text }}>
|
|
553
|
-
|
|
598
|
+
{tr("forge.checkout.payment_title")}
|
|
554
599
|
</h2>
|
|
555
600
|
<p className="text-sm mb-8" style={{ color: `${theme.colors.text}${alpha(0.7)}` }}>
|
|
556
|
-
|
|
601
|
+
{tr("forge.checkout.free_checkout_body")}
|
|
557
602
|
</p>
|
|
558
|
-
<PrimaryButton
|
|
603
|
+
<PrimaryButton
|
|
604
|
+
text={tr("forge.checkout.complete_checkout")}
|
|
605
|
+
onClick={handleFreeCheckout}
|
|
606
|
+
fullWidth
|
|
607
|
+
disabled={c.isFreeCheckoutLoading}
|
|
608
|
+
/>
|
|
559
609
|
</div>
|
|
560
610
|
)}
|
|
561
611
|
</div>
|
|
@@ -571,13 +621,13 @@ export function Checkout({
|
|
|
571
621
|
}}
|
|
572
622
|
>
|
|
573
623
|
<h3 className="text-xl font-bold mb-4" style={{ color: theme.colors.text }}>
|
|
574
|
-
|
|
624
|
+
{tr("forge.checkout.summary_title")}
|
|
575
625
|
</h3>
|
|
576
626
|
|
|
577
627
|
{/* Customer */}
|
|
578
628
|
<div className="mb-4 pb-4" style={{ borderBottom: `1px solid ${theme.colors.primary}${alpha(0.2)}` }}>
|
|
579
629
|
<h4 className="font-semibold mb-2" style={{ color: theme.colors.text }}>
|
|
580
|
-
|
|
630
|
+
{tr("forge.checkout.summary_customer")}
|
|
581
631
|
</h4>
|
|
582
632
|
{c.user ? (
|
|
583
633
|
<div className="text-sm">
|
|
@@ -595,7 +645,7 @@ export function Checkout({
|
|
|
595
645
|
</div>
|
|
596
646
|
) : (
|
|
597
647
|
<p className="text-sm" style={{ color: `${theme.colors.text}${alpha(0.5)}` }}>
|
|
598
|
-
|
|
648
|
+
{tr("forge.checkout.guest_checkout")}
|
|
599
649
|
</p>
|
|
600
650
|
)}
|
|
601
651
|
</div>
|
|
@@ -604,7 +654,7 @@ export function Checkout({
|
|
|
604
654
|
{c.hasPhysicalProduct && c.shippingData && (
|
|
605
655
|
<div className="mb-4 pb-4" style={{ borderBottom: `1px solid ${theme.colors.primary}${alpha(0.2)}` }}>
|
|
606
656
|
<h4 className="font-semibold mb-2" style={{ color: theme.colors.text }}>
|
|
607
|
-
|
|
657
|
+
{tr("forge.checkout.summary_shipping_address")}
|
|
608
658
|
</h4>
|
|
609
659
|
<div className="text-sm">
|
|
610
660
|
<p style={{ color: theme.colors.text }}>{c.shippingData.address1}</p>
|
|
@@ -621,7 +671,7 @@ export function Checkout({
|
|
|
621
671
|
{/* Cart Items */}
|
|
622
672
|
<div className="mb-4 pb-4" style={{ borderBottom: `1px solid ${theme.colors.primary}${alpha(0.2)}` }}>
|
|
623
673
|
<h4 className="font-semibold mb-2" style={{ color: theme.colors.text }}>
|
|
624
|
-
|
|
674
|
+
{tr("forge.checkout.summary_items")}
|
|
625
675
|
</h4>
|
|
626
676
|
<div className="space-y-2">
|
|
627
677
|
{/* Ticket lines first: they are what the bundle is FOR, and the
|
|
@@ -636,13 +686,13 @@ export function Checkout({
|
|
|
636
686
|
<div key={`${t.eventId}-${ticketId}`} className="flex justify-between text-sm" data-testid="summary-ticket-line">
|
|
637
687
|
<div className="flex-1">
|
|
638
688
|
<p className="font-medium" style={{ color: theme.colors.text }}>
|
|
639
|
-
{meta?.title ?? "
|
|
689
|
+
{meta?.title ?? tr("forge.checkout.ticket_fallback")}
|
|
640
690
|
</p>
|
|
641
691
|
<p className="text-xs" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
|
|
642
692
|
{t.eventTitle}
|
|
643
693
|
</p>
|
|
644
694
|
<p className="text-xs" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
|
|
645
|
-
|
|
695
|
+
{tr("forge.checkout.qty", { count: qty })}
|
|
646
696
|
</p>
|
|
647
697
|
</div>
|
|
648
698
|
<p className="font-medium" style={{ color: theme.colors.text }}>
|
|
@@ -667,11 +717,11 @@ export function Checkout({
|
|
|
667
717
|
<CartLineOptions item={item} accent={theme.colors.primary} size="small" />
|
|
668
718
|
{item.isGift && (
|
|
669
719
|
<p className="text-xs" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
|
|
670
|
-
|
|
720
|
+
{tr("forge.checkout.gift_for", { recipient_name: item.recipientName ?? "" })}
|
|
671
721
|
</p>
|
|
672
722
|
)}
|
|
673
723
|
<p className="text-xs" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
|
|
674
|
-
|
|
724
|
+
{tr("forge.checkout.qty", { count: item.quantity })}
|
|
675
725
|
</p>
|
|
676
726
|
</div>
|
|
677
727
|
<p className="font-medium" style={{ color: theme.colors.text }}>
|
|
@@ -698,25 +748,25 @@ export function Checkout({
|
|
|
698
748
|
style={{ color: theme.colors.primary }}
|
|
699
749
|
onClick={() => c.setShowCouponInput(true)}
|
|
700
750
|
>
|
|
701
|
-
|
|
751
|
+
{tr("forge.checkout.coupon_prompt")}
|
|
702
752
|
</button>
|
|
703
753
|
)}
|
|
704
754
|
{c.showCouponInput && !c.appliedCoupon && c.isCouponEditable && (
|
|
705
755
|
<div className="flex gap-2 items-end">
|
|
706
756
|
<div className="flex-1">
|
|
707
757
|
<p className="text-sm mb-1" style={{ color: theme.colors.text }}>
|
|
708
|
-
|
|
758
|
+
{tr("forge.checkout.coupon_label")}
|
|
709
759
|
</p>
|
|
710
760
|
<input
|
|
711
|
-
aria-label="
|
|
712
|
-
placeholder="
|
|
761
|
+
aria-label={tr("forge.checkout.aria_coupon_code")}
|
|
762
|
+
placeholder={tr("forge.checkout.coupon_placeholder")}
|
|
713
763
|
style={inputStyle}
|
|
714
764
|
value={c.couponCode}
|
|
715
765
|
onChange={(e) => c.setCouponCode(e.target.value.toUpperCase())}
|
|
716
766
|
/>
|
|
717
767
|
</div>
|
|
718
768
|
<PrimaryButton
|
|
719
|
-
text={c.isApplyingCoupon ? "..." : "
|
|
769
|
+
text={c.isApplyingCoupon ? "..." : tr("forge.checkout.coupon_apply")}
|
|
720
770
|
onClick={c.applyCoupon}
|
|
721
771
|
disabled={c.isApplyingCoupon || !c.couponCode.trim()}
|
|
722
772
|
/>
|
|
@@ -726,7 +776,7 @@ export function Checkout({
|
|
|
726
776
|
code is priced live, so promising "at payment" would be wrong. */}
|
|
727
777
|
{c.isBundle && !c.checkoutId && c.showCouponInput && !c.appliedCoupon && c.isCouponEditable && (
|
|
728
778
|
<p className="text-xs mt-1" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
|
|
729
|
-
|
|
779
|
+
{tr("forge.checkout.coupon_deferred_note")}
|
|
730
780
|
</p>
|
|
731
781
|
)}
|
|
732
782
|
{c.couponError && <p className="text-sm text-red-500 mt-1">{c.couponError}</p>}
|
|
@@ -737,19 +787,25 @@ export function Checkout({
|
|
|
737
787
|
>
|
|
738
788
|
<div>
|
|
739
789
|
<p className="text-sm font-semibold" style={{ color: theme.colors.primary }}>
|
|
740
|
-
|
|
790
|
+
{tr("forge.checkout.coupon_applied", { code: c.appliedCoupon.code })}
|
|
741
791
|
</p>
|
|
742
792
|
{/* A bundle reports the cash that came off and the code it
|
|
743
793
|
came off under, but not the rate — so say the cash,
|
|
744
794
|
never a made-up "0% off". */}
|
|
745
795
|
<p className="text-xs" style={{ color: theme.colors.text }}>
|
|
746
796
|
{c.appliedCoupon.discountType === "percentage"
|
|
747
|
-
?
|
|
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) })
|
|
748
800
|
: c.appliedCoupon.discountValue != null
|
|
749
|
-
?
|
|
801
|
+
? tr("forge.checkout.coupon_amount_off", {
|
|
802
|
+
amount: formatCurrency(c.appliedCoupon.discountValue),
|
|
803
|
+
})
|
|
750
804
|
: c.appliedCoupon.discountAmount != null
|
|
751
|
-
?
|
|
752
|
-
|
|
805
|
+
? tr("forge.checkout.coupon_amount_off", {
|
|
806
|
+
amount: formatCurrency(c.appliedCoupon.discountAmount),
|
|
807
|
+
})
|
|
808
|
+
: tr("forge.checkout.coupon_applied_at_payment")}
|
|
753
809
|
</p>
|
|
754
810
|
</div>
|
|
755
811
|
{c.isCouponEditable && (
|
|
@@ -760,7 +816,7 @@ export function Checkout({
|
|
|
760
816
|
onClick={c.removeCoupon}
|
|
761
817
|
disabled={c.isApplyingCoupon}
|
|
762
818
|
>
|
|
763
|
-
|
|
819
|
+
{tr("forge.checkout.coupon_remove")}
|
|
764
820
|
</button>
|
|
765
821
|
)}
|
|
766
822
|
</div>
|
|
@@ -771,18 +827,18 @@ export function Checkout({
|
|
|
771
827
|
{/* Totals */}
|
|
772
828
|
<div className="space-y-2">
|
|
773
829
|
<div className="flex justify-between">
|
|
774
|
-
<span style={{ color: theme.colors.text }}>
|
|
830
|
+
<span style={{ color: theme.colors.text }}>{tr("forge.checkout.subtotal")}</span>
|
|
775
831
|
<span style={{ color: theme.colors.text }}>{formatCurrency(c.subTotal)}</span>
|
|
776
832
|
</div>
|
|
777
833
|
{c.discountAmount > 0 && (
|
|
778
834
|
<div className="flex justify-between">
|
|
779
|
-
<span style={{ color: theme.colors.text }}>
|
|
835
|
+
<span style={{ color: theme.colors.text }}>{tr("forge.checkout.discount")}</span>
|
|
780
836
|
<span style={{ color: theme.colors.primary }}>-{formatCurrency(c.discountAmount)}</span>
|
|
781
837
|
</div>
|
|
782
838
|
)}
|
|
783
839
|
{c.shippingCost > 0 && (
|
|
784
840
|
<div className="flex justify-between">
|
|
785
|
-
<span style={{ color: theme.colors.text }}>
|
|
841
|
+
<span style={{ color: theme.colors.text }}>{tr("forge.checkout.shipping")}</span>
|
|
786
842
|
<span style={{ color: theme.colors.text }}>{formatCurrency(c.shippingCost)}</span>
|
|
787
843
|
</div>
|
|
788
844
|
)}
|
|
@@ -791,7 +847,7 @@ export function Checkout({
|
|
|
791
847
|
jumped past the subtotal with no explanation. */}
|
|
792
848
|
{c.bookingFeeCost > 0 && (
|
|
793
849
|
<div className="flex justify-between" data-testid="summary-booking-fee">
|
|
794
|
-
<span style={{ color: theme.colors.text }}>
|
|
850
|
+
<span style={{ color: theme.colors.text }}>{tr("forge.checkout.booking_fee")}</span>
|
|
795
851
|
<span style={{ color: theme.colors.text }}>{formatCurrency(c.bookingFeeCost)}</span>
|
|
796
852
|
</div>
|
|
797
853
|
)}
|
|
@@ -799,7 +855,7 @@ export function Checkout({
|
|
|
799
855
|
quote is known (added on top of the total). */}
|
|
800
856
|
{taxSummary?.mode === "exclusive" && (
|
|
801
857
|
<div className="flex justify-between">
|
|
802
|
-
<span style={{ color: theme.colors.text }}>
|
|
858
|
+
<span style={{ color: theme.colors.text }}>{tr("forge.checkout.tax")}</span>
|
|
803
859
|
<span style={{ color: theme.colors.text }}>{taxSummary.formattedTax}</span>
|
|
804
860
|
</div>
|
|
805
861
|
)}
|
|
@@ -807,7 +863,7 @@ export function Checkout({
|
|
|
807
863
|
className="flex justify-between font-bold text-lg pt-2"
|
|
808
864
|
style={{ borderTop: `1px solid ${theme.colors.primary}${alpha(0.2)}`, color: theme.colors.primary }}
|
|
809
865
|
>
|
|
810
|
-
<span>
|
|
866
|
+
<span>{tr("forge.checkout.total")}</span>
|
|
811
867
|
<span>
|
|
812
868
|
{c.chargedTotal
|
|
813
869
|
? formatCurrency(c.chargedTotal.amount, c.chargedTotal.currency)
|
|
@@ -824,7 +880,7 @@ export function Checkout({
|
|
|
824
880
|
!taxSummary &&
|
|
825
881
|
pricesIncludeTax && (
|
|
826
882
|
<p className="text-xs text-right" style={{ color: `${theme.colors.text}${alpha(0.6)}`, margin: 0 }}>
|
|
827
|
-
|
|
883
|
+
{tr("forge.checkout.incl_tax")}
|
|
828
884
|
</p>
|
|
829
885
|
)
|
|
830
886
|
)}
|
|
@@ -857,6 +913,7 @@ function PaymentSection({
|
|
|
857
913
|
onBack?: () => void;
|
|
858
914
|
}) {
|
|
859
915
|
const theme = useForgeTheme();
|
|
916
|
+
const tr = useForgeT();
|
|
860
917
|
const renderPay = usePaymentRenderer();
|
|
861
918
|
const { paymentSecret, chargedAmount, chargedCurrency, returnUrl, isPaystack, isCreatingOrder, startError } = checkout;
|
|
862
919
|
const { canOpenPaystack, openPaystackCheckout } = checkout;
|
|
@@ -878,7 +935,7 @@ function PaymentSection({
|
|
|
878
935
|
borderRadius: `${theme.cornerRadius}px`,
|
|
879
936
|
}}
|
|
880
937
|
>
|
|
881
|
-
|
|
938
|
+
{tr("forge.checkout.back")}
|
|
882
939
|
</button>
|
|
883
940
|
) : null;
|
|
884
941
|
|
|
@@ -894,10 +951,10 @@ function PaymentSection({
|
|
|
894
951
|
}}
|
|
895
952
|
>
|
|
896
953
|
<h2 className="text-2xl font-bold mb-2" style={{ color: theme.colors.text }}>
|
|
897
|
-
|
|
954
|
+
{tr("forge.checkout.payment_heading")}
|
|
898
955
|
</h2>
|
|
899
956
|
<p className="text-lg mb-4" style={{ color: theme.colors.text }}>
|
|
900
|
-
|
|
957
|
+
{tr("forge.checkout.payment_not_configured")}
|
|
901
958
|
</p>
|
|
902
959
|
{backButton}
|
|
903
960
|
</div>
|
|
@@ -914,7 +971,7 @@ function PaymentSection({
|
|
|
914
971
|
}}
|
|
915
972
|
>
|
|
916
973
|
<h2 className="text-2xl font-bold mb-6" style={{ color: theme.colors.text }}>
|
|
917
|
-
|
|
974
|
+
{tr("forge.checkout.payment_title")}
|
|
918
975
|
</h2>
|
|
919
976
|
|
|
920
977
|
{startError && (
|
|
@@ -932,13 +989,15 @@ function PaymentSection({
|
|
|
932
989
|
expiredMessage={holdExpiredError || undefined}
|
|
933
990
|
onRetry={retryHold}
|
|
934
991
|
isRetrying={isRetryingHold}
|
|
935
|
-
secondaryAction={onBack ? { label: "
|
|
992
|
+
secondaryAction={onBack ? { label: tr("forge.checkout.hold_change_order"), onClick: onBack } : undefined}
|
|
936
993
|
/>
|
|
937
994
|
|
|
938
995
|
{/* Suppressed once the server has refused: there is no intent coming, so a
|
|
939
996
|
"Preparing payment…" spinner would sit there forever under a notice
|
|
940
997
|
that has already explained what happened. */}
|
|
941
|
-
{isCreatingOrder && !hasCharge && !isHoldExpired &&
|
|
998
|
+
{isCreatingOrder && !hasCharge && !isHoldExpired && (
|
|
999
|
+
<Loading label={tr("forge.checkout.preparing_payment")} size={22} />
|
|
1000
|
+
)}
|
|
942
1001
|
|
|
943
1002
|
{isPaystack
|
|
944
1003
|
? canOpenPaystack && (
|
|
@@ -985,6 +1044,7 @@ function ShippingRateSelector({
|
|
|
985
1044
|
formatCurrency: (amount: number, currencyCode?: string) => string;
|
|
986
1045
|
}) {
|
|
987
1046
|
const theme = useForgeTheme();
|
|
1047
|
+
const tr = useForgeT();
|
|
988
1048
|
if (groups.length === 0) return null;
|
|
989
1049
|
|
|
990
1050
|
return (
|
|
@@ -1011,7 +1071,7 @@ function ShippingRateSelector({
|
|
|
1011
1071
|
</span>
|
|
1012
1072
|
{group.rates[0].estimatedDays != null && (
|
|
1013
1073
|
<span className="text-xs ml-2" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
|
|
1014
|
-
({group.rates[0].estimatedDays}
|
|
1074
|
+
{tr("forge.checkout.rate_days", { days: group.rates[0].estimatedDays })}
|
|
1015
1075
|
</span>
|
|
1016
1076
|
)}
|
|
1017
1077
|
</div>
|
|
@@ -1024,7 +1084,7 @@ function ShippingRateSelector({
|
|
|
1024
1084
|
<div className="space-y-2">
|
|
1025
1085
|
{group.rates.length === 0 && (
|
|
1026
1086
|
<p className="text-sm" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
|
|
1027
|
-
|
|
1087
|
+
{tr("forge.checkout.no_shipping_options")}
|
|
1028
1088
|
</p>
|
|
1029
1089
|
)}
|
|
1030
1090
|
{group.rates.map((rate) => {
|
|
@@ -1053,7 +1113,9 @@ function ShippingRateSelector({
|
|
|
1053
1113
|
</span>
|
|
1054
1114
|
{rate.estimatedDays && (
|
|
1055
1115
|
<p className="text-xs" style={{ color: `${theme.colors.text}${alpha(0.6)}` }}>
|
|
1056
|
-
|
|
1116
|
+
{rate.estimatedDays === 1
|
|
1117
|
+
? tr("forge.checkout.rate_estimated_day", { days: rate.estimatedDays })
|
|
1118
|
+
: tr("forge.checkout.rate_estimated_days", { days: rate.estimatedDays })}
|
|
1057
1119
|
</p>
|
|
1058
1120
|
)}
|
|
1059
1121
|
</div>
|