@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
|
@@ -2,6 +2,7 @@ import type { CSSProperties } from "react";
|
|
|
2
2
|
import { useThemeTokens } from "../theme/ForgeThemeProvider";
|
|
3
3
|
import { useCourseClassroom, type UseCourseClassroomOptions } from "../headless/course/useCourseClassroom";
|
|
4
4
|
import { Loading } from "./Loading";
|
|
5
|
+
import { useForgeT } from "../../i18n";
|
|
5
6
|
|
|
6
7
|
export interface CourseAccessProps extends UseCourseClassroomOptions {
|
|
7
8
|
/** Course access id (route param). */
|
|
@@ -36,7 +37,8 @@ export function CourseAccess({
|
|
|
36
37
|
style,
|
|
37
38
|
...classroomOptions
|
|
38
39
|
}: CourseAccessProps) {
|
|
39
|
-
const t =
|
|
40
|
+
const t = useForgeT();
|
|
41
|
+
const tokens = useThemeTokens();
|
|
40
42
|
const classroom = useCourseClassroom(accessId, classroomOptions);
|
|
41
43
|
const go = navigate ?? ((href: string) => window.location.assign(href));
|
|
42
44
|
|
|
@@ -46,24 +48,24 @@ export function CourseAccess({
|
|
|
46
48
|
maxWidth: 520,
|
|
47
49
|
margin: "0 auto",
|
|
48
50
|
padding: 28,
|
|
49
|
-
borderRadius:
|
|
50
|
-
border: `1px solid ${
|
|
51
|
-
background:
|
|
52
|
-
color:
|
|
51
|
+
borderRadius: tokens.cornerRadius,
|
|
52
|
+
border: `1px solid ${tokens.border}`,
|
|
53
|
+
background: tokens.surface,
|
|
54
|
+
color: tokens.text,
|
|
53
55
|
textAlign: "center",
|
|
54
56
|
display: "flex",
|
|
55
57
|
flexDirection: "column",
|
|
56
58
|
gap: 12,
|
|
57
|
-
fontFamily:
|
|
59
|
+
fontFamily: tokens.fontFamily,
|
|
58
60
|
};
|
|
59
61
|
|
|
60
62
|
const primaryButton: CSSProperties = {
|
|
61
63
|
alignSelf: "center",
|
|
62
64
|
padding: "10px 20px",
|
|
63
|
-
borderRadius:
|
|
65
|
+
borderRadius: tokens.cornerRadius,
|
|
64
66
|
border: "none",
|
|
65
|
-
background:
|
|
66
|
-
color:
|
|
67
|
+
background: tokens.primary,
|
|
68
|
+
color: tokens.textPrimary,
|
|
67
69
|
fontSize: 14,
|
|
68
70
|
fontWeight: 700,
|
|
69
71
|
cursor: "pointer",
|
|
@@ -74,17 +76,17 @@ export function CourseAccess({
|
|
|
74
76
|
if (gate) {
|
|
75
77
|
return (
|
|
76
78
|
<div className={className} style={{ ...panel, ...style }}>
|
|
77
|
-
<h1 style={{ fontSize: 20, fontWeight: 800, fontFamily:
|
|
79
|
+
<h1 style={{ fontSize: 20, fontWeight: 800, fontFamily: tokens.headingFontFamily || tokens.fontFamily }}>
|
|
78
80
|
{gate.title}
|
|
79
81
|
</h1>
|
|
80
|
-
<p style={{ fontSize: 14, color:
|
|
82
|
+
<p style={{ fontSize: 14, color: tokens.muted, lineHeight: 1.6 }}>{gate.body}</p>
|
|
81
83
|
|
|
82
84
|
{/* Naming the session they are in is what makes "wrong account"
|
|
83
85
|
actionable — otherwise the reader has no idea which of their
|
|
84
86
|
addresses they are currently signed in with. */}
|
|
85
87
|
{gate.kind === "wrong_account" && classroom.signedInEmail && (
|
|
86
|
-
<p style={{ fontSize: 13, color:
|
|
87
|
-
|
|
88
|
+
<p style={{ fontSize: 13, color: tokens.muted }}>
|
|
89
|
+
{t("forge.course_access.signed_in_as")} <strong style={{ color: tokens.text }}>{classroom.signedInEmail}</strong>
|
|
88
90
|
</p>
|
|
89
91
|
)}
|
|
90
92
|
|
|
@@ -120,8 +122,8 @@ export function CourseAccess({
|
|
|
120
122
|
gap: 24,
|
|
121
123
|
maxWidth: 1100,
|
|
122
124
|
margin: "0 auto",
|
|
123
|
-
color:
|
|
124
|
-
fontFamily:
|
|
125
|
+
color: tokens.text,
|
|
126
|
+
fontFamily: tokens.fontFamily,
|
|
125
127
|
...style,
|
|
126
128
|
}}
|
|
127
129
|
>
|
|
@@ -135,56 +137,59 @@ export function CourseAccess({
|
|
|
135
137
|
alignItems: "center",
|
|
136
138
|
gap: 12,
|
|
137
139
|
padding: "10px 14px",
|
|
138
|
-
borderRadius:
|
|
139
|
-
border: `1px solid ${
|
|
140
|
-
background: `${
|
|
140
|
+
borderRadius: tokens.cornerRadius,
|
|
141
|
+
border: `1px solid ${tokens.border}`,
|
|
142
|
+
background: `${tokens.primary}0d`,
|
|
141
143
|
fontSize: 13,
|
|
142
144
|
}}
|
|
143
145
|
>
|
|
144
|
-
<span style={{ flex: "1 1 260px", color:
|
|
145
|
-
|
|
146
|
-
<strong style={{ color:
|
|
146
|
+
<span style={{ flex: "1 1 260px", color: tokens.muted, lineHeight: 1.5 }}>
|
|
147
|
+
{t("forge.course_access.secure_prompt_lead")}{" "}
|
|
148
|
+
<strong style={{ color: tokens.text }}>{securePrompt.email}</strong> {t("forge.course_access.secure_prompt_tail")}
|
|
147
149
|
</span>
|
|
148
150
|
<button
|
|
149
151
|
type="button"
|
|
150
152
|
onClick={() => go(securePrompt.href)}
|
|
151
153
|
style={{
|
|
152
154
|
padding: "6px 14px",
|
|
153
|
-
borderRadius:
|
|
154
|
-
border: `1px solid ${
|
|
155
|
+
borderRadius: tokens.cornerRadius,
|
|
156
|
+
border: `1px solid ${tokens.primary}`,
|
|
155
157
|
background: "transparent",
|
|
156
|
-
color:
|
|
158
|
+
color: tokens.primary,
|
|
157
159
|
fontSize: 13,
|
|
158
160
|
fontWeight: 600,
|
|
159
161
|
cursor: "pointer",
|
|
160
162
|
}}
|
|
161
163
|
>
|
|
162
|
-
|
|
164
|
+
{t("forge.course_access.secure_action")}
|
|
163
165
|
</button>
|
|
164
166
|
<button
|
|
165
167
|
type="button"
|
|
166
168
|
onClick={classroom.dismissSecurePrompt}
|
|
167
|
-
aria-label="
|
|
169
|
+
aria-label={t("forge.course_access.aria_dismiss")}
|
|
168
170
|
style={{
|
|
169
171
|
padding: "6px 10px",
|
|
170
172
|
border: "none",
|
|
171
173
|
background: "transparent",
|
|
172
|
-
color:
|
|
174
|
+
color: tokens.muted,
|
|
173
175
|
fontSize: 13,
|
|
174
176
|
cursor: "pointer",
|
|
175
177
|
}}
|
|
176
178
|
>
|
|
177
|
-
|
|
179
|
+
{t("forge.course_access.dismiss")}
|
|
178
180
|
</button>
|
|
179
181
|
</div>
|
|
180
182
|
)}
|
|
181
183
|
|
|
182
184
|
<header>
|
|
183
|
-
<h1 style={{ fontSize: 26, fontWeight: 800, fontFamily:
|
|
185
|
+
<h1 style={{ fontSize: 26, fontWeight: 800, fontFamily: tokens.headingFontFamily || tokens.fontFamily }}>
|
|
184
186
|
{data.course.title}
|
|
185
187
|
</h1>
|
|
186
|
-
<p style={{ fontSize: 14, color:
|
|
187
|
-
{
|
|
188
|
+
<p style={{ fontSize: 14, color: tokens.muted }}>
|
|
189
|
+
{t("forge.course_access.lessons_completed", {
|
|
190
|
+
completed: classroom.completedCount,
|
|
191
|
+
total: classroom.lessons.length,
|
|
192
|
+
})}
|
|
188
193
|
</p>
|
|
189
194
|
</header>
|
|
190
195
|
|
|
@@ -205,12 +210,12 @@ export function CourseAccess({
|
|
|
205
210
|
style={{
|
|
206
211
|
textAlign: "left",
|
|
207
212
|
padding: "8px 10px",
|
|
208
|
-
borderRadius:
|
|
213
|
+
borderRadius: tokens.cornerRadius,
|
|
209
214
|
border: "none",
|
|
210
215
|
cursor: "pointer",
|
|
211
216
|
fontSize: 14,
|
|
212
|
-
background: isSelected ? `${
|
|
213
|
-
color: isSelected ?
|
|
217
|
+
background: isSelected ? `${tokens.primary}20` : "transparent",
|
|
218
|
+
color: isSelected ? tokens.primary : tokens.text,
|
|
214
219
|
fontWeight: isSelected ? 600 : 400,
|
|
215
220
|
}}
|
|
216
221
|
>
|
|
@@ -235,7 +240,7 @@ export function CourseAccess({
|
|
|
235
240
|
controls
|
|
236
241
|
playsInline
|
|
237
242
|
onTimeUpdate={(e) => classroom.reportProgress(e.currentTarget.currentTime, e.currentTarget.duration)}
|
|
238
|
-
style={{ width: "100%", aspectRatio: "16/9", background: "#000", borderRadius:
|
|
243
|
+
style={{ width: "100%", aspectRatio: "16/9", background: "#000", borderRadius: tokens.cornerRadius }}
|
|
239
244
|
/>
|
|
240
245
|
) : (
|
|
241
246
|
<div
|
|
@@ -245,12 +250,12 @@ export function CourseAccess({
|
|
|
245
250
|
display: "flex",
|
|
246
251
|
alignItems: "center",
|
|
247
252
|
justifyContent: "center",
|
|
248
|
-
background: `${
|
|
249
|
-
borderRadius:
|
|
250
|
-
color:
|
|
253
|
+
background: `${tokens.text}0d`,
|
|
254
|
+
borderRadius: tokens.cornerRadius,
|
|
255
|
+
color: tokens.muted,
|
|
251
256
|
}}
|
|
252
257
|
>
|
|
253
|
-
|
|
258
|
+
{t("forge.course_access.no_video")}
|
|
254
259
|
</div>
|
|
255
260
|
)}
|
|
256
261
|
|
|
@@ -259,7 +264,7 @@ export function CourseAccess({
|
|
|
259
264
|
fontSize: 20,
|
|
260
265
|
fontWeight: 700,
|
|
261
266
|
margin: "16px 0 8px",
|
|
262
|
-
fontFamily:
|
|
267
|
+
fontFamily: tokens.headingFontFamily || tokens.fontFamily,
|
|
263
268
|
}}
|
|
264
269
|
>
|
|
265
270
|
{selected.title}
|
|
@@ -267,7 +272,7 @@ export function CourseAccess({
|
|
|
267
272
|
|
|
268
273
|
{files.length > 0 && (
|
|
269
274
|
<div style={{ marginBottom: 16 }}>
|
|
270
|
-
<h3 style={{ fontWeight: 600, marginBottom: 8 }}>
|
|
275
|
+
<h3 style={{ fontWeight: 600, marginBottom: 8 }}>{t("forge.course_access.resources_title")}</h3>
|
|
271
276
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
272
277
|
{files.map((file) => (
|
|
273
278
|
<a
|
|
@@ -279,16 +284,18 @@ export function CourseAccess({
|
|
|
279
284
|
display: "flex",
|
|
280
285
|
justifyContent: "space-between",
|
|
281
286
|
padding: "10px 12px",
|
|
282
|
-
borderRadius:
|
|
283
|
-
border: `1px solid ${
|
|
284
|
-
background: `${
|
|
285
|
-
color:
|
|
287
|
+
borderRadius: tokens.cornerRadius,
|
|
288
|
+
border: `1px solid ${tokens.border}`,
|
|
289
|
+
background: `${tokens.primary}10`,
|
|
290
|
+
color: tokens.text,
|
|
286
291
|
textDecoration: "none",
|
|
287
292
|
fontSize: 14,
|
|
288
293
|
}}
|
|
289
294
|
>
|
|
290
295
|
<span>{file.filename}</span>
|
|
291
|
-
<span style={{ color:
|
|
296
|
+
<span style={{ color: tokens.muted }}>
|
|
297
|
+
{t("forge.course_access.file_size_mb", { size: (file.size / 1024 / 1024).toFixed(2) })}
|
|
298
|
+
</span>
|
|
292
299
|
</a>
|
|
293
300
|
))}
|
|
294
301
|
</div>
|
|
@@ -300,7 +307,7 @@ export function CourseAccess({
|
|
|
300
307
|
)}
|
|
301
308
|
</>
|
|
302
309
|
) : (
|
|
303
|
-
<p style={{ color:
|
|
310
|
+
<p style={{ color: tokens.muted }}>{t("forge.course_access.no_lesson_selected")}</p>
|
|
304
311
|
)}
|
|
305
312
|
</main>
|
|
306
313
|
</div>
|
|
@@ -2,6 +2,7 @@ import { type CSSProperties, type ReactNode, useMemo, useState } from "react";
|
|
|
2
2
|
import { X } from "lucide-react";
|
|
3
3
|
import type { AppliedDiscountCoupon, PublicCourse, QuestionnaireQuestion } from "../../types/models";
|
|
4
4
|
import { useCourseCheckout } from "../headless/course/useCourseCheckout";
|
|
5
|
+
import { useForgeT } from "../../i18n";
|
|
5
6
|
import { useForgeTheme, type ForgeTheme } from "../theme/ForgeThemeProvider";
|
|
6
7
|
import { useAmountFormatter } from "../format/useFormatCurrency";
|
|
7
8
|
import { summarizeTaxQuote } from "../format/PriceDisplay";
|
|
@@ -54,12 +55,13 @@ const a = (hex: string, alpha: number) => hex + Math.round(alpha * 255).toString
|
|
|
54
55
|
*/
|
|
55
56
|
export function CourseCheckout({
|
|
56
57
|
slug,
|
|
57
|
-
triggerText
|
|
58
|
+
triggerText,
|
|
58
59
|
formatAmount,
|
|
59
60
|
renderPayment,
|
|
60
61
|
finalisePath,
|
|
61
62
|
onComplete,
|
|
62
63
|
}: CourseCheckoutProps) {
|
|
64
|
+
const t = useForgeT();
|
|
63
65
|
const theme = useForgeTheme();
|
|
64
66
|
const registered = usePaymentRenderer();
|
|
65
67
|
const fmt = useAmountFormatter(formatAmount);
|
|
@@ -70,7 +72,7 @@ export function CourseCheckout({
|
|
|
70
72
|
|
|
71
73
|
const tryClose = (next: boolean) => {
|
|
72
74
|
if (!next && c.step !== "details" && typeof window !== "undefined") {
|
|
73
|
-
if (!window.confirm("
|
|
75
|
+
if (!window.confirm(t("forge.course_checkout.confirm_leave"))) return;
|
|
74
76
|
}
|
|
75
77
|
setOpen(next);
|
|
76
78
|
};
|
|
@@ -78,7 +80,7 @@ export function CourseCheckout({
|
|
|
78
80
|
return (
|
|
79
81
|
<DialogRoot open={open} onOpenChange={tryClose}>
|
|
80
82
|
<DialogTrigger asChild>
|
|
81
|
-
<button style={triggerButtonStyle(theme)}>{triggerText}</button>
|
|
83
|
+
<button style={triggerButtonStyle(theme)}>{triggerText ?? t("forge.course_checkout.trigger")}</button>
|
|
82
84
|
</DialogTrigger>
|
|
83
85
|
<DialogPortal>
|
|
84
86
|
<DialogOverlay style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)", zIndex: 50 }} />
|
|
@@ -101,16 +103,18 @@ export function CourseCheckout({
|
|
|
101
103
|
aria-describedby={undefined}
|
|
102
104
|
>
|
|
103
105
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
|
|
104
|
-
<DialogTitle style={{ fontSize: 18, fontWeight: 700, margin: 0 }}>
|
|
106
|
+
<DialogTitle style={{ fontSize: 18, fontWeight: 700, margin: 0 }}>
|
|
107
|
+
{c.course?.title ?? t("forge.course_checkout.title")}
|
|
108
|
+
</DialogTitle>
|
|
105
109
|
<DialogClose asChild>
|
|
106
|
-
<button aria-label="
|
|
110
|
+
<button aria-label={t("forge.course_checkout.aria_close")} style={closeButtonStyle(theme)}>
|
|
107
111
|
<X size={18} />
|
|
108
112
|
</button>
|
|
109
113
|
</DialogClose>
|
|
110
114
|
</div>
|
|
111
115
|
|
|
112
116
|
{c.isLoading && <Loading />}
|
|
113
|
-
{!c.isLoading && !c.course && <p style={{ marginTop: 8 }}>
|
|
117
|
+
{!c.isLoading && !c.course && <p style={{ marginTop: 8 }}>{t("forge.course_checkout.not_found")}</p>}
|
|
114
118
|
{c.course && <CourseCheckoutBody c={c} course={c.course} fmt={fmt} renderPayment={renderPay} />}
|
|
115
119
|
</DialogContent>
|
|
116
120
|
</DialogPortal>
|
|
@@ -184,6 +188,7 @@ function Progress({ current }: { current: number }) {
|
|
|
184
188
|
|
|
185
189
|
// ── Step 1: buyer details + questionnaire ─────────────────────────────────────
|
|
186
190
|
function DetailsStep({ c, course, fmt }: { c: Checkout; course: PublicCourse; fmt: (n: number) => string }) {
|
|
191
|
+
const t = useForgeT();
|
|
187
192
|
const theme = useForgeTheme();
|
|
188
193
|
const [confirmEmail, setConfirmEmail] = useState("");
|
|
189
194
|
const [answers, setAnswers] = useState<Record<string, string>>({});
|
|
@@ -202,38 +207,43 @@ function DetailsStep({ c, course, fmt }: { c: Checkout; course: PublicCourse; fm
|
|
|
202
207
|
|
|
203
208
|
const onContinue = () => {
|
|
204
209
|
setLocalError(null);
|
|
205
|
-
if (!c.firstName.trim() || !c.lastName.trim()) return setLocalError("
|
|
206
|
-
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(c.email)) return setLocalError("
|
|
207
|
-
if (c.email !== confirmEmail) return setLocalError("
|
|
210
|
+
if (!c.firstName.trim() || !c.lastName.trim()) return setLocalError(t("forge.course_checkout.error_name_required"));
|
|
211
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(c.email)) return setLocalError(t("forge.course_checkout.error_email_invalid"));
|
|
212
|
+
if (c.email !== confirmEmail) return setLocalError(t("forge.course_checkout.error_email_mismatch"));
|
|
208
213
|
for (const q of questions) {
|
|
209
|
-
if (!q.optional && !(answers[q.id] ?? "").trim())
|
|
214
|
+
if (!q.optional && !(answers[q.id] ?? "").trim())
|
|
215
|
+
return setLocalError(t("forge.course_checkout.error_answer_required", { question: q.question }));
|
|
210
216
|
}
|
|
211
217
|
c.continueToPayment();
|
|
212
218
|
};
|
|
213
219
|
|
|
214
220
|
return (
|
|
215
221
|
<div>
|
|
216
|
-
<h2 style={{ fontSize: 22, fontWeight: 700, marginBottom: 4 }}>
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
</p>
|
|
222
|
+
<h2 style={{ fontSize: 22, fontWeight: 700, marginBottom: 4 }}>
|
|
223
|
+
{isFree ? t("forge.course_checkout.free_title") : t("forge.course_checkout.details_title")}
|
|
224
|
+
</h2>
|
|
225
|
+
<p style={{ fontSize: 14, opacity: 0.7, marginBottom: 16 }}>{t("forge.course_checkout.details_subtitle")}</p>
|
|
220
226
|
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
221
227
|
<div style={{ display: "flex", gap: 8 }}>
|
|
222
|
-
<input placeholder="
|
|
223
|
-
<input placeholder="
|
|
228
|
+
<input placeholder={t("forge.course_checkout.first_name_placeholder")} value={c.firstName} onChange={(e) => c.setFirstName(e.target.value)} style={inputStyle(theme)} />
|
|
229
|
+
<input placeholder={t("forge.course_checkout.last_name_placeholder")} value={c.lastName} onChange={(e) => c.setLastName(e.target.value)} style={inputStyle(theme)} />
|
|
224
230
|
</div>
|
|
225
231
|
<div>
|
|
226
|
-
<input placeholder="
|
|
227
|
-
{c.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(c.email) &&
|
|
232
|
+
<input placeholder={t("forge.course_checkout.email_placeholder")} type="email" value={c.email} onChange={(e) => c.setEmail(e.target.value)} style={inputStyle(theme)} />
|
|
233
|
+
{c.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(c.email) && (
|
|
234
|
+
<FieldError>{t("forge.course_checkout.email_invalid_hint")}</FieldError>
|
|
235
|
+
)}
|
|
228
236
|
</div>
|
|
229
237
|
<div>
|
|
230
|
-
<input placeholder="
|
|
231
|
-
{confirmEmail && c.email !== confirmEmail &&
|
|
238
|
+
<input placeholder={t("forge.course_checkout.confirm_email_placeholder")} type="email" value={confirmEmail} onChange={(e) => setConfirmEmail(e.target.value)} style={inputStyle(theme)} />
|
|
239
|
+
{confirmEmail && c.email !== confirmEmail && (
|
|
240
|
+
<FieldError>{t("forge.course_checkout.email_mismatch_hint")}</FieldError>
|
|
241
|
+
)}
|
|
232
242
|
</div>
|
|
233
243
|
|
|
234
244
|
{questions.length > 0 && (
|
|
235
245
|
<div style={{ marginTop: 8, paddingTop: 16, borderTop: `1px solid ${a(theme.colors.primary, 0.13)}` }}>
|
|
236
|
-
<h3 style={{ fontSize: 17, fontWeight: 700, marginBottom: 12 }}>
|
|
246
|
+
<h3 style={{ fontSize: 17, fontWeight: 700, marginBottom: 12 }}>{t("forge.course_checkout.questions_title")}</h3>
|
|
237
247
|
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
238
248
|
{questions.map((q) => {
|
|
239
249
|
const missing = !q.optional && !(answers[q.id] ?? "").trim();
|
|
@@ -245,7 +255,7 @@ function DetailsStep({ c, course, fmt }: { c: Checkout; course: PublicCourse; fm
|
|
|
245
255
|
</span>
|
|
246
256
|
{q.type === "select" && q.options && q.options.length > 0 ? (
|
|
247
257
|
<select value={answers[q.id] ?? ""} onChange={(e) => onAnswer(q.id, e.target.value)} style={inputStyle(theme)}>
|
|
248
|
-
<option value="">
|
|
258
|
+
<option value="">{t("forge.course_checkout.select_placeholder")}</option>
|
|
249
259
|
{q.options.map((opt) => (
|
|
250
260
|
<option key={opt} value={opt}>
|
|
251
261
|
{opt}
|
|
@@ -262,7 +272,7 @@ function DetailsStep({ c, course, fmt }: { c: Checkout; course: PublicCourse; fm
|
|
|
262
272
|
) : (
|
|
263
273
|
<input value={answers[q.id] ?? ""} onChange={(e) => onAnswer(q.id, e.target.value)} style={inputStyle(theme)} />
|
|
264
274
|
)}
|
|
265
|
-
{missing && <FieldError>
|
|
275
|
+
{missing && <FieldError>{t("forge.course_checkout.field_required")}</FieldError>}
|
|
266
276
|
</label>
|
|
267
277
|
);
|
|
268
278
|
})}
|
|
@@ -279,9 +289,7 @@ function DetailsStep({ c, course, fmt }: { c: Checkout; course: PublicCourse; fm
|
|
|
279
289
|
</div>
|
|
280
290
|
)}
|
|
281
291
|
|
|
282
|
-
<p style={{ fontSize: 12, opacity: 0.6 }}>
|
|
283
|
-
By continuing, you agree to receive course access and important updates via email.
|
|
284
|
-
</p>
|
|
292
|
+
<p style={{ fontSize: 12, opacity: 0.6 }}>{t("forge.course_checkout.consent")}</p>
|
|
285
293
|
|
|
286
294
|
{(localError || c.error) && <p style={{ color: "#ef4444", fontSize: 14 }}>{localError ?? c.error}</p>}
|
|
287
295
|
</div>
|
|
@@ -293,7 +301,13 @@ function DetailsStep({ c, course, fmt }: { c: Checkout; course: PublicCourse; fm
|
|
|
293
301
|
discountAmount={c.coupon.discountAmount}
|
|
294
302
|
appliedCoupons={c.coupon.appliedCoupons}
|
|
295
303
|
onNext={onContinue}
|
|
296
|
-
nextLabel={
|
|
304
|
+
nextLabel={
|
|
305
|
+
c.isProcessing
|
|
306
|
+
? t("forge.course_checkout.processing")
|
|
307
|
+
: isFree
|
|
308
|
+
? t("forge.course_checkout.confirm_purchase")
|
|
309
|
+
: t("forge.course_checkout.continue_to_payment")
|
|
310
|
+
}
|
|
297
311
|
nextDisabled={c.isProcessing}
|
|
298
312
|
/>
|
|
299
313
|
</div>
|
|
@@ -312,6 +326,7 @@ function PaymentStep({
|
|
|
312
326
|
fmt: (n: number) => string;
|
|
313
327
|
renderPayment?: (props: PaymentRenderProps) => ReactNode;
|
|
314
328
|
}) {
|
|
329
|
+
const t = useForgeT();
|
|
315
330
|
const theme = useForgeTheme();
|
|
316
331
|
// The server's figure once it exists — the booking quote is already net of
|
|
317
332
|
// any discount, so the buyer never sees a total that disagrees with the charge.
|
|
@@ -322,8 +337,8 @@ function PaymentStep({
|
|
|
322
337
|
const taxSummary = summarizeTaxQuote(c.taxQuote);
|
|
323
338
|
return (
|
|
324
339
|
<div>
|
|
325
|
-
<h2 style={{ fontSize: 22, fontWeight: 700, marginBottom: 8 }}>
|
|
326
|
-
<p style={{ fontSize: 14, opacity: 0.7, marginBottom: 16 }}>
|
|
340
|
+
<h2 style={{ fontSize: 22, fontWeight: 700, marginBottom: 8 }}>{t("forge.course_checkout.payment_title")}</h2>
|
|
341
|
+
<p style={{ fontSize: 14, opacity: 0.7, marginBottom: 16 }}>{t("forge.course_checkout.payment_subtitle")}</p>
|
|
327
342
|
<div
|
|
328
343
|
style={{
|
|
329
344
|
padding: 20,
|
|
@@ -339,7 +354,7 @@ function PaymentStep({
|
|
|
339
354
|
fmt={fmt}
|
|
340
355
|
/>
|
|
341
356
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
342
|
-
<span style={{ fontSize: 16 }}>
|
|
357
|
+
<span style={{ fontSize: 16 }}>{t("forge.course_checkout.total_amount")}</span>
|
|
343
358
|
<span style={{ fontSize: 22, fontWeight: 700, color: theme.colors.primary }}>
|
|
344
359
|
{compareAt != null && compareAt > amount && (
|
|
345
360
|
<span style={{ textDecoration: "line-through", opacity: 0.6, marginRight: 8, fontWeight: 400, fontSize: 16 }}>
|
|
@@ -349,7 +364,9 @@ function PaymentStep({
|
|
|
349
364
|
{fmt(amount)}
|
|
350
365
|
{taxSummary && (
|
|
351
366
|
<span style={{ display: "block", fontSize: 12, fontWeight: 400, color: theme.colors.text, opacity: 0.65 }}>
|
|
352
|
-
{taxSummary.mode === "inclusive"
|
|
367
|
+
{taxSummary.mode === "inclusive"
|
|
368
|
+
? taxSummary.label
|
|
369
|
+
: t("forge.course_checkout.tax_added", { amount: taxSummary.formattedTax })}
|
|
353
370
|
</span>
|
|
354
371
|
)}
|
|
355
372
|
</span>
|
|
@@ -359,19 +376,19 @@ function PaymentStep({
|
|
|
359
376
|
so it is checked BEFORE the "not configured" branch. */}
|
|
360
377
|
{c.isPaystack ? (
|
|
361
378
|
c.canOpenPaystack ? (
|
|
362
|
-
<PaystackPayButton onPay={c.openPaystackCheckout} label={
|
|
379
|
+
<PaystackPayButton onPay={c.openPaystackCheckout} label={t("forge.course_checkout.pay", { amount: fmt(amount) })} />
|
|
363
380
|
) : (
|
|
364
|
-
<p style={{ opacity: 0.7 }}>
|
|
381
|
+
<p style={{ opacity: 0.7 }}>{t("forge.course_checkout.preparing_payment")}</p>
|
|
365
382
|
)
|
|
366
383
|
) : !renderPayment ? (
|
|
367
|
-
<p style={{ color: "#ef4444", fontSize: 14 }}>
|
|
384
|
+
<p style={{ color: "#ef4444", fontSize: 14 }}>{t("forge.course_checkout.payment_not_configured")}</p>
|
|
368
385
|
) : c.clientSecret ? (
|
|
369
386
|
renderPayment({ clientSecret: c.clientSecret, returnUrl: c.returnUrl, amount, mode: "redirect" })
|
|
370
387
|
) : (
|
|
371
|
-
<p style={{ opacity: 0.7 }}>
|
|
388
|
+
<p style={{ opacity: 0.7 }}>{t("forge.course_checkout.preparing_payment")}</p>
|
|
372
389
|
)}
|
|
373
390
|
<button onClick={() => c.setStep("details")} style={{ ...secondaryButtonStyle(theme), marginTop: 16 }}>
|
|
374
|
-
|
|
391
|
+
{t("forge.course_checkout.back")}
|
|
375
392
|
</button>
|
|
376
393
|
</div>
|
|
377
394
|
);
|
|
@@ -405,6 +422,7 @@ function ActionBar({
|
|
|
405
422
|
nextLabel: string;
|
|
406
423
|
nextDisabled?: boolean;
|
|
407
424
|
}) {
|
|
425
|
+
const t = useForgeT();
|
|
408
426
|
const theme = useForgeTheme();
|
|
409
427
|
return (
|
|
410
428
|
<div style={{ marginTop: 24 }}>
|
|
@@ -423,7 +441,7 @@ function ActionBar({
|
|
|
423
441
|
fmt={fmt}
|
|
424
442
|
/>
|
|
425
443
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
426
|
-
<span style={{ fontWeight: 600 }}>
|
|
444
|
+
<span style={{ fontWeight: 600 }}>{t("forge.course_checkout.total")}</span>
|
|
427
445
|
<span style={{ fontSize: 18, fontWeight: 700, color: theme.colors.primary }}>
|
|
428
446
|
{compareAt != null && compareAt > amount && (
|
|
429
447
|
<span style={{ textDecoration: "line-through", opacity: 0.6, marginRight: 8, fontWeight: 400, fontSize: 14 }}>
|
|
@@ -436,7 +454,7 @@ function ActionBar({
|
|
|
436
454
|
</div>
|
|
437
455
|
<div style={{ display: "flex", gap: 8, justifyContent: "space-between" }}>
|
|
438
456
|
<button onClick={onBack} disabled={!onBack} style={{ ...secondaryButtonStyle(theme), opacity: onBack ? 1 : 0.4 }}>
|
|
439
|
-
|
|
457
|
+
{t("forge.course_checkout.back")}
|
|
440
458
|
</button>
|
|
441
459
|
<button onClick={onNext} disabled={nextDisabled} style={{ ...primaryButtonStyle(theme), opacity: nextDisabled ? 0.6 : 1 }}>
|
|
442
460
|
{nextLabel}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
ConfirmationLostCheckout,
|
|
14
14
|
alpha,
|
|
15
15
|
} from "./Confirmation";
|
|
16
|
+
import { useForgeT } from "../../i18n";
|
|
16
17
|
import { useCourseBookingFinalize } from "../../data/queries/useFinalize";
|
|
17
18
|
import { useGetCourse } from "../../data/queries/useCourses";
|
|
18
19
|
import { useBookingSecret } from "../headless/booking/useBookingSecret";
|
|
@@ -39,6 +40,7 @@ export function CourseConfirmation({
|
|
|
39
40
|
restartPath,
|
|
40
41
|
}: CourseConfirmationProps) {
|
|
41
42
|
const theme = useForgeTheme();
|
|
43
|
+
const t = useForgeT();
|
|
42
44
|
const fmt = useAmountFormatter(formatAmount);
|
|
43
45
|
// The credential the checkout stashed before the payment redirect. `resolved`
|
|
44
46
|
// gates the finalise: storage can only be read on the client, so firing before
|
|
@@ -57,7 +59,7 @@ export function CourseConfirmation({
|
|
|
57
59
|
if (!resolved || isLoading) {
|
|
58
60
|
return (
|
|
59
61
|
<ConfirmationStage>
|
|
60
|
-
<Loading label="
|
|
62
|
+
<Loading label={t("forge.course_confirmation.loading")} />
|
|
61
63
|
</ConfirmationStage>
|
|
62
64
|
);
|
|
63
65
|
}
|
|
@@ -70,17 +72,17 @@ export function CourseConfirmation({
|
|
|
70
72
|
return (
|
|
71
73
|
<ConfirmationStage>
|
|
72
74
|
<ConfirmationLostCheckout
|
|
73
|
-
title=
|
|
74
|
-
body="
|
|
75
|
+
title={t("forge.course_confirmation.lost_title")}
|
|
76
|
+
body={t("forge.course_confirmation.lost_body")}
|
|
75
77
|
restartPath={restartPath ?? `/i/courses/${slug}`}
|
|
76
|
-
restartLabel="
|
|
78
|
+
restartLabel={t("forge.course_confirmation.lost_restart")}
|
|
77
79
|
/>
|
|
78
80
|
</ConfirmationStage>
|
|
79
81
|
);
|
|
80
82
|
}
|
|
81
83
|
return (
|
|
82
84
|
<ConfirmationStage>
|
|
83
|
-
<p style={{ color: text }}>
|
|
85
|
+
<p style={{ color: text }}>{t("forge.course_confirmation.not_found")}</p>
|
|
84
86
|
</ConfirmationStage>
|
|
85
87
|
);
|
|
86
88
|
}
|
|
@@ -107,15 +109,21 @@ export function CourseConfirmation({
|
|
|
107
109
|
animationDelay: "0.15s",
|
|
108
110
|
}}
|
|
109
111
|
>
|
|
110
|
-
{confirmed
|
|
112
|
+
{confirmed
|
|
113
|
+
? t("forge.course_confirmation.eyebrow_confirmed")
|
|
114
|
+
: t("forge.course_confirmation.eyebrow_action_needed")}
|
|
111
115
|
</p>
|
|
112
116
|
<h1 className="cf-rise" style={{ fontSize: 29, fontWeight: 800, lineHeight: 1.12, margin: "0 0 8px", animationDelay: "0.22s" }}>
|
|
113
|
-
{confirmed
|
|
117
|
+
{confirmed
|
|
118
|
+
? t("forge.course_confirmation.title_confirmed")
|
|
119
|
+
: t("forge.course_confirmation.title_incomplete")}
|
|
114
120
|
</h1>
|
|
115
121
|
<p className="cf-rise" style={{ fontSize: 15, color: alpha(text, 0.7), margin: "0 auto", maxWidth: 340, animationDelay: "0.3s" }}>
|
|
116
122
|
{confirmed
|
|
117
|
-
?
|
|
118
|
-
|
|
123
|
+
? data.firstName
|
|
124
|
+
? t("forge.course_confirmation.body_confirmed", { first_name: data.firstName })
|
|
125
|
+
: t("forge.course_confirmation.body_confirmed_no_name")
|
|
126
|
+
: t("forge.course_confirmation.body_incomplete", { status: data.status })}
|
|
119
127
|
</p>
|
|
120
128
|
</div>
|
|
121
129
|
|
|
@@ -143,7 +151,9 @@ export function CourseConfirmation({
|
|
|
143
151
|
<div style={{ minWidth: 0 }}>
|
|
144
152
|
<div style={{ fontSize: 16, fontWeight: 800 }}>{course.title}</div>
|
|
145
153
|
{course.modules?.length ? (
|
|
146
|
-
<div style={{ fontSize: 13, color: alpha(text, 0.6) }}>
|
|
154
|
+
<div style={{ fontSize: 13, color: alpha(text, 0.6) }}>
|
|
155
|
+
{t("forge.course_confirmation.module_count", { count: course.modules.length })}
|
|
156
|
+
</div>
|
|
147
157
|
) : null}
|
|
148
158
|
</div>
|
|
149
159
|
</div>
|
|
@@ -177,20 +187,30 @@ export function CourseConfirmation({
|
|
|
177
187
|
</div>
|
|
178
188
|
)}
|
|
179
189
|
<dl style={{ display: "grid", gridTemplateColumns: "1fr auto", rowGap: 12, columnGap: 16, margin: 0 }}>
|
|
180
|
-
<ConfirmationRow
|
|
181
|
-
|
|
190
|
+
<ConfirmationRow
|
|
191
|
+
label={t("forge.course_confirmation.row_student")}
|
|
192
|
+
value={`${data.firstName} ${data.lastName}`.trim() || t("forge.course_confirmation.not_given")}
|
|
193
|
+
/>
|
|
194
|
+
<ConfirmationRow
|
|
195
|
+
label={t("forge.course_confirmation.row_reference")}
|
|
196
|
+
value={`#${data.id.slice(0, 8).toUpperCase()}`}
|
|
197
|
+
mono
|
|
198
|
+
/>
|
|
182
199
|
{Number(data.taxAmount || 0) > 0 && !data.taxInclusive && (
|
|
183
|
-
<ConfirmationRow
|
|
200
|
+
<ConfirmationRow
|
|
201
|
+
label={t("forge.course_confirmation.row_tax")}
|
|
202
|
+
value={fmt(Number(data.taxAmount), data.chargedCurrency || undefined)}
|
|
203
|
+
/>
|
|
184
204
|
)}
|
|
185
205
|
{/* The exact charged total (charged currency) when present — never a re-converted catalog amount. */}
|
|
186
206
|
<ConfirmationRow
|
|
187
|
-
label="
|
|
207
|
+
label={t("forge.course_confirmation.row_paid")}
|
|
188
208
|
value={
|
|
189
209
|
Number(data.totalAmountInChargedCurrency ?? data.totalAmount) > 0
|
|
190
210
|
? data.totalAmountInChargedCurrency
|
|
191
211
|
? fmt(Number(data.totalAmountInChargedCurrency), data.chargedCurrency || undefined)
|
|
192
212
|
: fmt(data.totalAmount)
|
|
193
|
-
: "
|
|
213
|
+
: t("forge.course_confirmation.free")
|
|
194
214
|
}
|
|
195
215
|
accent
|
|
196
216
|
strong
|
|
@@ -198,7 +218,9 @@ export function CourseConfirmation({
|
|
|
198
218
|
</dl>
|
|
199
219
|
{Number(data.taxAmount || 0) > 0 && data.taxInclusive && (
|
|
200
220
|
<div style={{ textAlign: "right", fontSize: 12, color: alpha(text, 0.6), marginTop: 6 }}>
|
|
201
|
-
|
|
221
|
+
{t("forge.course_confirmation.tax_included", {
|
|
222
|
+
amount: fmt(Number(data.taxAmount), data.chargedCurrency || undefined),
|
|
223
|
+
})}
|
|
202
224
|
</div>
|
|
203
225
|
)}
|
|
204
226
|
</div>
|
|
@@ -221,7 +243,7 @@ export function CourseConfirmation({
|
|
|
221
243
|
textDecoration: "none",
|
|
222
244
|
}}
|
|
223
245
|
>
|
|
224
|
-
|
|
246
|
+
{t("forge.course_confirmation.explore")} <ArrowRight size={16} />
|
|
225
247
|
</a>
|
|
226
248
|
</div>
|
|
227
249
|
</ConfirmationCard>
|