@tribe-nest/forge 3.26.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/activeLocale.ts +34 -0
- package/src/client/createForgeClient.ts +13 -0
- package/src/contexts/CartContext.tsx +76 -0
- package/src/contexts/_tests/CartContext.spec.tsx +210 -0
- package/src/data/queries/useCheckouts.ts +83 -0
- 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 +9 -2
- 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 +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 +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/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 +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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useForgeT } from "../../i18n";
|
|
1
2
|
import { useInvoicePayment } from "../headless/invoice/useInvoicePayment";
|
|
2
3
|
import { usePaymentRenderer } from "../payment/ForgePaymentProvider";
|
|
3
4
|
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
@@ -26,13 +27,15 @@ const formatDate = (value: string | number | Date): string =>
|
|
|
26
27
|
* or Paystack's inline modal behind a pay button.
|
|
27
28
|
*/
|
|
28
29
|
export function InvoicePayment({ invoiceId, formatAmount, returnPath }: InvoicePaymentProps) {
|
|
30
|
+
const t = useForgeT();
|
|
29
31
|
const theme = useForgeTheme();
|
|
30
32
|
const renderPayment = usePaymentRenderer();
|
|
31
33
|
const { invoice, downloadPdf, clientSecret, returnUrl, isStarting, error, taxQuote, isPaystack, canOpenPaystack, openPaystackCheckout } =
|
|
32
34
|
useInvoicePayment(invoiceId, { returnPath });
|
|
33
35
|
|
|
34
36
|
if (invoice.isLoading) return <Loading fullPage />;
|
|
35
|
-
if (invoice.error || !invoice.data)
|
|
37
|
+
if (invoice.error || !invoice.data)
|
|
38
|
+
return <p style={{ color: theme.colors.text }}>{t("forge.invoice_payment.not_found")}</p>;
|
|
36
39
|
|
|
37
40
|
const inv = invoice.data;
|
|
38
41
|
const isPaid = inv.status === "paid" || !!inv.paidAt;
|
|
@@ -72,7 +75,9 @@ export function InvoicePayment({ invoiceId, formatAmount, returnPath }: InvoiceP
|
|
|
72
75
|
<div style={{ maxWidth: 640, margin: "0 auto", color: theme.colors.text }}>
|
|
73
76
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 16 }}>
|
|
74
77
|
<div>
|
|
75
|
-
<h1 style={{ fontSize: 26, fontWeight: 800 }}>
|
|
78
|
+
<h1 style={{ fontSize: 26, fontWeight: 800 }}>
|
|
79
|
+
{t("forge.invoice_payment.title", { invoice_number: inv.invoiceNumber })}
|
|
80
|
+
</h1>
|
|
76
81
|
<p style={{ opacity: 0.7, fontSize: 14 }}>{inv.profileName}</p>
|
|
77
82
|
</div>
|
|
78
83
|
<button
|
|
@@ -88,18 +93,26 @@ export function InvoicePayment({ invoiceId, formatAmount, returnPath }: InvoiceP
|
|
|
88
93
|
fontSize: 14,
|
|
89
94
|
}}
|
|
90
95
|
>
|
|
91
|
-
{downloadPdf.isPending ? "
|
|
96
|
+
{downloadPdf.isPending ? t("forge.invoice_payment.preparing_pdf") : t("forge.invoice_payment.download_pdf")}
|
|
92
97
|
</button>
|
|
93
98
|
</div>
|
|
94
99
|
|
|
95
100
|
<div style={{ fontSize: 14, opacity: 0.85, marginBottom: 16 }}>
|
|
96
101
|
<div>
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
{inv.clientCompany
|
|
103
|
+
? t("forge.invoice_payment.billed_to_company", {
|
|
104
|
+
client_name: inv.clientName,
|
|
105
|
+
client_company: inv.clientCompany,
|
|
106
|
+
})
|
|
107
|
+
: t("forge.invoice_payment.billed_to", { client_name: inv.clientName })}
|
|
99
108
|
</div>
|
|
100
109
|
<div>
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
{inv.dueDate
|
|
111
|
+
? t("forge.invoice_payment.issued_due", {
|
|
112
|
+
issue_date: formatDate(inv.issueDate),
|
|
113
|
+
due_date: formatDate(inv.dueDate),
|
|
114
|
+
})
|
|
115
|
+
: t("forge.invoice_payment.issued", { issue_date: formatDate(inv.issueDate) })}
|
|
103
116
|
</div>
|
|
104
117
|
</div>
|
|
105
118
|
|
|
@@ -123,7 +136,10 @@ export function InvoicePayment({ invoiceId, formatAmount, returnPath }: InvoiceP
|
|
|
123
136
|
>
|
|
124
137
|
<span>
|
|
125
138
|
{li.description}
|
|
126
|
-
<span style={{ opacity: 0.6 }}>
|
|
139
|
+
<span style={{ opacity: 0.6 }}>
|
|
140
|
+
{" "}
|
|
141
|
+
{t("forge.invoice_payment.line_item_quantity", { quantity: li.quantity })}
|
|
142
|
+
</span>
|
|
127
143
|
</span>
|
|
128
144
|
<span>{money(Number(li.amount))}</span>
|
|
129
145
|
</div>
|
|
@@ -142,13 +158,13 @@ export function InvoicePayment({ invoiceId, formatAmount, returnPath }: InvoiceP
|
|
|
142
158
|
opacity: 0.8,
|
|
143
159
|
}}
|
|
144
160
|
>
|
|
145
|
-
<span>
|
|
161
|
+
<span>{t("forge.invoice_payment.tax")}</span>
|
|
146
162
|
<span>{taxSummary.formattedTax}</span>
|
|
147
163
|
</div>
|
|
148
164
|
) : null;
|
|
149
165
|
})()}
|
|
150
166
|
<div style={{ display: "flex", justifyContent: "space-between", padding: "12px 14px", fontWeight: 700 }}>
|
|
151
|
-
<span>
|
|
167
|
+
<span>{t("forge.invoice_payment.total")}</span>
|
|
152
168
|
<span style={{ color: theme.colors.primary, textAlign: "right" }}>
|
|
153
169
|
{money(Number(inv.total))}
|
|
154
170
|
{(() => {
|
|
@@ -168,18 +184,23 @@ export function InvoicePayment({ invoiceId, formatAmount, returnPath }: InvoiceP
|
|
|
168
184
|
{inv.notes && <p style={{ fontSize: 13, opacity: 0.7, marginBottom: 16 }}>{inv.notes}</p>}
|
|
169
185
|
|
|
170
186
|
{isPaid ? (
|
|
171
|
-
<p style={{ fontWeight: 700, color: theme.colors.primary }}>
|
|
187
|
+
<p style={{ fontWeight: 700, color: theme.colors.primary }}>{t("forge.invoice_payment.paid_notice")}</p>
|
|
172
188
|
) : (
|
|
173
189
|
<div>
|
|
174
|
-
<h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: 12 }}>
|
|
190
|
+
<h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: 12 }}>
|
|
191
|
+
{t("forge.invoice_payment.payment_heading")}
|
|
192
|
+
</h2>
|
|
175
193
|
{error && <p style={{ color: "#ef4444", fontSize: 14, marginBottom: 8 }}>{error}</p>}
|
|
176
194
|
{/* Paystack brings its own modal, so it is checked before the Stripe
|
|
177
195
|
renderer and needs none of it. */}
|
|
178
196
|
{isPaystack ? (
|
|
179
197
|
canOpenPaystack ? (
|
|
180
|
-
<PaystackPayButton
|
|
198
|
+
<PaystackPayButton
|
|
199
|
+
onPay={openPaystackCheckout}
|
|
200
|
+
label={t("forge.invoice_payment.pay_amount", { amount: money(Number(inv.total)) })}
|
|
201
|
+
/>
|
|
181
202
|
) : (
|
|
182
|
-
<Loading label="
|
|
203
|
+
<Loading label={t("forge.invoice_payment.preparing_payment")} size={22} />
|
|
183
204
|
)
|
|
184
205
|
) : clientSecret ? (
|
|
185
206
|
renderPayment?.({
|
|
@@ -191,9 +212,9 @@ export function InvoicePayment({ invoiceId, formatAmount, returnPath }: InvoiceP
|
|
|
191
212
|
) : (
|
|
192
213
|
<>
|
|
193
214
|
{isStarting ? (
|
|
194
|
-
<Loading label="
|
|
215
|
+
<Loading label={t("forge.invoice_payment.preparing_payment")} size={22} />
|
|
195
216
|
) : (
|
|
196
|
-
<p style={{ opacity: 0.7 }}>
|
|
217
|
+
<p style={{ opacity: 0.7 }}>{t("forge.invoice_payment.payment_unavailable")}</p>
|
|
197
218
|
)}
|
|
198
219
|
</>
|
|
199
220
|
)}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { useLeadMagnetConfirm, useTrackLeadMagnetDownload } from "../../data/queries/useLeadMagnet";
|
|
3
|
+
import { useForgeT } from "../../i18n";
|
|
3
4
|
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
4
5
|
import { Loading } from "./Loading";
|
|
5
6
|
|
|
@@ -12,6 +13,7 @@ export interface LeadMagnetProps {
|
|
|
12
13
|
|
|
13
14
|
/** Lead magnet delivery / download screen. Built on `useLeadMagnetConfirm` + `useTrackLeadMagnetDownload`. */
|
|
14
15
|
export function LeadMagnet({ leadMagnetId, subscriberId }: LeadMagnetProps) {
|
|
16
|
+
const t = useForgeT();
|
|
15
17
|
const theme = useForgeTheme();
|
|
16
18
|
const trackDownload = useTrackLeadMagnetDownload();
|
|
17
19
|
const [isDownloading, setIsDownloading] = useState(false);
|
|
@@ -57,8 +59,10 @@ export function LeadMagnet({ leadMagnetId, subscriberId }: LeadMagnetProps) {
|
|
|
57
59
|
if (state === "error") {
|
|
58
60
|
return (
|
|
59
61
|
<div style={wrap}>
|
|
60
|
-
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 12 }}>
|
|
61
|
-
<p>
|
|
62
|
+
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 12 }}>{t("forge.lead_magnet.error_title")}</h1>
|
|
63
|
+
<p>
|
|
64
|
+
{!leadMagnetId || !subscriberId ? t("forge.lead_magnet.invalid_link") : t("forge.lead_magnet.expired_link")}
|
|
65
|
+
</p>
|
|
62
66
|
</div>
|
|
63
67
|
);
|
|
64
68
|
}
|
|
@@ -67,11 +71,9 @@ export function LeadMagnet({ leadMagnetId, subscriberId }: LeadMagnetProps) {
|
|
|
67
71
|
<div style={wrap}>
|
|
68
72
|
{profile && <p style={{ fontSize: 14, opacity: 0.7, marginBottom: 8 }}>{profile.name}</p>}
|
|
69
73
|
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 12 }}>
|
|
70
|
-
{leadMagnet?.thankYouMessage || "
|
|
74
|
+
{leadMagnet?.thankYouMessage || t("forge.lead_magnet.thank_you")}
|
|
71
75
|
</h1>
|
|
72
|
-
<p style={{ opacity: 0.8, marginBottom: 32 }}>
|
|
73
|
-
Your download is ready. Click the button below to get your file.
|
|
74
|
-
</p>
|
|
76
|
+
<p style={{ opacity: 0.8, marginBottom: 32 }}>{t("forge.lead_magnet.description")}</p>
|
|
75
77
|
<button
|
|
76
78
|
onClick={handleDownload}
|
|
77
79
|
disabled={isDownloading}
|
|
@@ -86,7 +88,7 @@ export function LeadMagnet({ leadMagnetId, subscriberId }: LeadMagnetProps) {
|
|
|
86
88
|
color: theme.colors.background,
|
|
87
89
|
}}
|
|
88
90
|
>
|
|
89
|
-
{isDownloading ? "
|
|
91
|
+
{isDownloading ? t("forge.lead_magnet.preparing") : t("forge.lead_magnet.download")}
|
|
90
92
|
</button>
|
|
91
93
|
</div>
|
|
92
94
|
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useForgeT } from "../../i18n";
|
|
1
2
|
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
2
3
|
|
|
3
4
|
export interface LoadingProps {
|
|
@@ -14,6 +15,7 @@ const alpha = (hex: string, a: number) => hex + Math.round(a * 255).toString(16)
|
|
|
14
15
|
/** Themed loading spinner — drop-in from `@tribe-nest/forge/ui`. */
|
|
15
16
|
export function Loading({ size = 28, label, fullPage }: LoadingProps) {
|
|
16
17
|
const theme = useForgeTheme();
|
|
18
|
+
const t = useForgeT();
|
|
17
19
|
const border = Math.max(2, Math.round(size / 9));
|
|
18
20
|
|
|
19
21
|
const content = (
|
|
@@ -45,7 +47,11 @@ export function Loading({ size = 28, label, fullPage }: LoadingProps) {
|
|
|
45
47
|
}}
|
|
46
48
|
/>
|
|
47
49
|
{label && <span style={{ fontSize: 14, opacity: 0.7 }}>{label}</span>}
|
|
48
|
-
{!label &&
|
|
50
|
+
{!label && (
|
|
51
|
+
<span style={{ position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" }}>
|
|
52
|
+
{t("forge.loading.loading")}
|
|
53
|
+
</span>
|
|
54
|
+
)}
|
|
49
55
|
</div>
|
|
50
56
|
);
|
|
51
57
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useLoginFlow } from "../headless/auth/useLoginFlow";
|
|
2
2
|
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
3
|
+
import { useForgeT } from "../../i18n";
|
|
3
4
|
|
|
4
5
|
export interface LoginFormProps {
|
|
5
6
|
/** Called after a successful login. The shell typically redirects here. */
|
|
@@ -12,6 +13,7 @@ export interface LoginFormProps {
|
|
|
12
13
|
|
|
13
14
|
/** Themed 2FA login form (credentials → emailed code), built on `useLoginFlow`. */
|
|
14
15
|
export function LoginForm({ onSuccess, signupHref, forgotHref }: LoginFormProps) {
|
|
16
|
+
const t = useForgeT();
|
|
15
17
|
const theme = useForgeTheme();
|
|
16
18
|
const flow = useLoginFlow({ onSuccess });
|
|
17
19
|
|
|
@@ -48,9 +50,9 @@ export function LoginForm({ onSuccess, signupHref, forgotHref }: LoginFormProps)
|
|
|
48
50
|
if (flow.step === "code") {
|
|
49
51
|
return (
|
|
50
52
|
<div style={card}>
|
|
51
|
-
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 8 }}>
|
|
53
|
+
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 8 }}>{t("forge.login_form.verify_title")}</h1>
|
|
52
54
|
<p style={{ fontSize: 14, opacity: 0.7, marginBottom: 24 }}>
|
|
53
|
-
|
|
55
|
+
{t("forge.login_form.code_sent_to")} <strong>{flow.email}</strong>
|
|
54
56
|
</p>
|
|
55
57
|
{flow.error && <p style={{ color: "#ef4444", marginBottom: 16 }}>{flow.error}</p>}
|
|
56
58
|
<form
|
|
@@ -71,12 +73,12 @@ export function LoginForm({ onSuccess, signupHref, forgotHref }: LoginFormProps)
|
|
|
71
73
|
style={{ ...input, textAlign: "center", fontSize: 24, letterSpacing: "0.4em" }}
|
|
72
74
|
/>
|
|
73
75
|
<button type="submit" disabled={flow.isSubmitting || flow.code.length !== 6} style={button}>
|
|
74
|
-
{flow.isSubmitting ? "
|
|
76
|
+
{flow.isSubmitting ? t("forge.login_form.verifying") : t("forge.login_form.verify_submit")}
|
|
75
77
|
</button>
|
|
76
78
|
</form>
|
|
77
79
|
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 16, fontSize: 14 }}>
|
|
78
80
|
<button type="button" onClick={flow.back} style={{ ...link, background: "none", border: "none", cursor: "pointer" }}>
|
|
79
|
-
|
|
81
|
+
{t("forge.login_form.back_to_login")}
|
|
80
82
|
</button>
|
|
81
83
|
<button
|
|
82
84
|
type="button"
|
|
@@ -84,7 +86,7 @@ export function LoginForm({ onSuccess, signupHref, forgotHref }: LoginFormProps)
|
|
|
84
86
|
disabled={flow.isSubmitting}
|
|
85
87
|
style={{ ...link, background: "none", border: "none", cursor: "pointer" }}
|
|
86
88
|
>
|
|
87
|
-
|
|
89
|
+
{t("forge.login_form.resend_code")}
|
|
88
90
|
</button>
|
|
89
91
|
</div>
|
|
90
92
|
</div>
|
|
@@ -93,7 +95,7 @@ export function LoginForm({ onSuccess, signupHref, forgotHref }: LoginFormProps)
|
|
|
93
95
|
|
|
94
96
|
return (
|
|
95
97
|
<div style={card}>
|
|
96
|
-
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 24 }}>
|
|
98
|
+
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 24 }}>{t("forge.login_form.title")}</h1>
|
|
97
99
|
{flow.error && <p style={{ color: "#ef4444", marginBottom: 16 }}>{flow.error}</p>}
|
|
98
100
|
<form
|
|
99
101
|
onSubmit={(e) => {
|
|
@@ -103,30 +105,36 @@ export function LoginForm({ onSuccess, signupHref, forgotHref }: LoginFormProps)
|
|
|
103
105
|
style={{ display: "flex", flexDirection: "column", gap: 16 }}
|
|
104
106
|
>
|
|
105
107
|
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
106
|
-
<label style={{ fontSize: 14 }}>
|
|
107
|
-
<input
|
|
108
|
+
<label style={{ fontSize: 14 }}>{t("forge.login_form.email_label")}</label>
|
|
109
|
+
<input
|
|
110
|
+
type="email"
|
|
111
|
+
placeholder={t("forge.login_form.email_placeholder")}
|
|
112
|
+
value={flow.email}
|
|
113
|
+
onChange={(e) => flow.setEmail(e.target.value)}
|
|
114
|
+
style={input}
|
|
115
|
+
/>
|
|
108
116
|
</div>
|
|
109
117
|
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
110
|
-
<label style={{ fontSize: 14 }}>
|
|
118
|
+
<label style={{ fontSize: 14 }}>{t("forge.login_form.password_label")}</label>
|
|
111
119
|
<input
|
|
112
120
|
type="password"
|
|
113
|
-
placeholder="
|
|
121
|
+
placeholder={t("forge.login_form.password_placeholder")}
|
|
114
122
|
value={flow.password}
|
|
115
123
|
onChange={(e) => flow.setPassword(e.target.value)}
|
|
116
124
|
style={input}
|
|
117
125
|
/>
|
|
118
126
|
<a href={forgotHref} style={{ ...link, fontSize: 13 }}>
|
|
119
|
-
|
|
127
|
+
{t("forge.login_form.forgot_password")}
|
|
120
128
|
</a>
|
|
121
129
|
</div>
|
|
122
130
|
<button type="submit" disabled={flow.isSubmitting} style={button}>
|
|
123
|
-
{flow.isSubmitting ? "
|
|
131
|
+
{flow.isSubmitting ? t("forge.login_form.sending_code") : t("forge.login_form.submit")}
|
|
124
132
|
</button>
|
|
125
133
|
</form>
|
|
126
134
|
<p style={{ marginTop: 16, fontSize: 14, textAlign: "center" }}>
|
|
127
|
-
|
|
135
|
+
{t("forge.login_form.no_account")}{" "}
|
|
128
136
|
<a href={signupHref} style={link}>
|
|
129
|
-
|
|
137
|
+
{t("forge.login_form.sign_up")}
|
|
130
138
|
</a>
|
|
131
139
|
</p>
|
|
132
140
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CSSProperties } from "react";
|
|
2
2
|
import { useMembershipCheckout } from "../headless/membership/useMembershipCheckout";
|
|
3
|
+
import { useForgeT } from "../../i18n";
|
|
3
4
|
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
4
5
|
import { useAmountFormatter } from "../format/useFormatCurrency";
|
|
5
6
|
import { priceTaxCaption } from "../format/PriceDisplay";
|
|
@@ -44,6 +45,7 @@ export function MembershipCheckout({
|
|
|
44
45
|
className,
|
|
45
46
|
style,
|
|
46
47
|
}: MembershipCheckoutProps) {
|
|
48
|
+
const t = useForgeT();
|
|
47
49
|
const theme = useForgeTheme();
|
|
48
50
|
const fmt = useAmountFormatter(formatAmount);
|
|
49
51
|
// Memberships have no checkout tax quote (subscription rail) — inclusive
|
|
@@ -81,9 +83,9 @@ export function MembershipCheckout({
|
|
|
81
83
|
if (!tier) {
|
|
82
84
|
return (
|
|
83
85
|
<div className={className} style={card}>
|
|
84
|
-
<p>
|
|
86
|
+
<p>{t("forge.membership_checkout.no_tier_body")}</p>
|
|
85
87
|
<a href={membershipsPath} style={{ ...button, marginTop: 16, display: "inline-block", textAlign: "center", textDecoration: "none" }}>
|
|
86
|
-
|
|
88
|
+
{t("forge.membership_checkout.view_memberships")}
|
|
87
89
|
</a>
|
|
88
90
|
</div>
|
|
89
91
|
);
|
|
@@ -96,13 +98,20 @@ export function MembershipCheckout({
|
|
|
96
98
|
if (checkout.step === "payment" && (checkout.clientSecret || checkout.canOpenPaystack)) {
|
|
97
99
|
return (
|
|
98
100
|
<div className={className} style={card}>
|
|
99
|
-
<h1 style={{ fontSize: 26, fontWeight: 800, marginBottom: 8 }}>
|
|
101
|
+
<h1 style={{ fontSize: 26, fontWeight: 800, marginBottom: 8 }}>{t("forge.membership_checkout.payment_title")}</h1>
|
|
100
102
|
<p style={{ opacity: 0.75, marginBottom: 24 }}>
|
|
101
|
-
{
|
|
103
|
+
{t("forge.membership_checkout.payment_summary", {
|
|
104
|
+
tier_name: tier.name,
|
|
105
|
+
amount: fmt(checkout.amount),
|
|
106
|
+
cycle: checkout.billingCycle,
|
|
107
|
+
})}
|
|
102
108
|
</p>
|
|
103
109
|
<div style={box}>
|
|
104
110
|
{checkout.isPaystack ? (
|
|
105
|
-
<PaystackPayButton
|
|
111
|
+
<PaystackPayButton
|
|
112
|
+
onPay={checkout.openPaystackCheckout}
|
|
113
|
+
label={t("forge.membership_checkout.pay", { amount: fmt(checkout.amount) })}
|
|
114
|
+
/>
|
|
106
115
|
) : (
|
|
107
116
|
checkout.clientSecret &&
|
|
108
117
|
renderPayment({
|
|
@@ -119,7 +128,7 @@ export function MembershipCheckout({
|
|
|
119
128
|
onClick={() => checkout.setStep("select")}
|
|
120
129
|
style={{ ...button, background: "transparent", color: theme.colors.text, border: `1px solid ${theme.colors.primary}40` }}
|
|
121
130
|
>
|
|
122
|
-
|
|
131
|
+
{t("forge.membership_checkout.back")}
|
|
123
132
|
</button>
|
|
124
133
|
</div>
|
|
125
134
|
);
|
|
@@ -129,7 +138,9 @@ export function MembershipCheckout({
|
|
|
129
138
|
|
|
130
139
|
return (
|
|
131
140
|
<div className={className} style={card}>
|
|
132
|
-
<h1 style={{ fontSize: 26, fontWeight: 800, marginBottom: 24 }}>
|
|
141
|
+
<h1 style={{ fontSize: 26, fontWeight: 800, marginBottom: 24 }}>
|
|
142
|
+
{t("forge.membership_checkout.title", { tier_name: tier.name })}
|
|
143
|
+
</h1>
|
|
133
144
|
|
|
134
145
|
<div style={box}>
|
|
135
146
|
{tier.description && <p style={{ marginBottom: 16 }}>{tier.description}</p>}
|
|
@@ -148,7 +159,7 @@ export function MembershipCheckout({
|
|
|
148
159
|
<div style={box}>
|
|
149
160
|
{showBillingToggle && (
|
|
150
161
|
<div style={{ marginBottom: 16 }}>
|
|
151
|
-
<p style={{ fontSize: 14, marginBottom: 8 }}>
|
|
162
|
+
<p style={{ fontSize: 14, marginBottom: 8 }}>{t("forge.membership_checkout.billing_cycle_label")}</p>
|
|
152
163
|
<div style={{ display: "flex", gap: 16 }}>
|
|
153
164
|
{(["month", "year"] as const).map((cycle) => (
|
|
154
165
|
<label key={cycle} style={{ display: "flex", alignItems: "center", gap: 6, cursor: "pointer" }}>
|
|
@@ -159,7 +170,9 @@ export function MembershipCheckout({
|
|
|
159
170
|
onChange={() => checkout.setBillingCycle(cycle)}
|
|
160
171
|
style={{ accentColor: theme.colors.primary }}
|
|
161
172
|
/>
|
|
162
|
-
{cycle === "month"
|
|
173
|
+
{cycle === "month"
|
|
174
|
+
? t("forge.membership_checkout.billing_monthly")
|
|
175
|
+
: t("forge.membership_checkout.billing_yearly")}
|
|
163
176
|
</label>
|
|
164
177
|
))}
|
|
165
178
|
</div>
|
|
@@ -168,7 +181,9 @@ export function MembershipCheckout({
|
|
|
168
181
|
|
|
169
182
|
{tier.payWhatYouWant && (
|
|
170
183
|
<div style={{ marginBottom: 16 }}>
|
|
171
|
-
<label style={{ fontSize: 14, display: "block", marginBottom: 8 }}>
|
|
184
|
+
<label style={{ fontSize: 14, display: "block", marginBottom: 8 }}>
|
|
185
|
+
{t("forge.membership_checkout.pwyw_label")}
|
|
186
|
+
</label>
|
|
172
187
|
<input
|
|
173
188
|
type="number"
|
|
174
189
|
min={tier.payWhatYouWantMinimum ?? 0}
|
|
@@ -189,9 +204,13 @@ export function MembershipCheckout({
|
|
|
189
204
|
)}
|
|
190
205
|
|
|
191
206
|
<div style={{ textAlign: "center", paddingTop: 16, borderTop: `1px solid ${theme.colors.primary}20` }}>
|
|
192
|
-
<p style={{ fontSize: 14, opacity: 0.75 }}>
|
|
207
|
+
<p style={{ fontSize: 14, opacity: 0.75 }}>{t("forge.membership_checkout.total")}</p>
|
|
193
208
|
<p style={{ fontSize: 28, fontWeight: 800 }}>{fmt(checkout.amount)}</p>
|
|
194
|
-
<p style={{ fontSize: 14, opacity: 0.75 }}>
|
|
209
|
+
<p style={{ fontSize: 14, opacity: 0.75 }}>
|
|
210
|
+
{checkout.billingCycle === "month"
|
|
211
|
+
? t("forge.membership_checkout.per_month")
|
|
212
|
+
: t("forge.membership_checkout.per_year")}
|
|
213
|
+
</p>
|
|
195
214
|
{taxCaption && <p style={{ fontSize: 12, opacity: 0.65, marginTop: 2 }}>{taxCaption}</p>}
|
|
196
215
|
</div>
|
|
197
216
|
</div>
|
|
@@ -199,7 +218,7 @@ export function MembershipCheckout({
|
|
|
199
218
|
{checkout.error && <p style={{ color: "#ef4444", marginBottom: 16 }}>{checkout.error}</p>}
|
|
200
219
|
|
|
201
220
|
<button onClick={() => checkout.subscribe()} disabled={checkout.isProcessing} style={button}>
|
|
202
|
-
{checkout.isProcessing ? "
|
|
221
|
+
{checkout.isProcessing ? t("forge.membership_checkout.submitting") : t("forge.membership_checkout.submit")}
|
|
203
222
|
</button>
|
|
204
223
|
</div>
|
|
205
224
|
);
|
|
@@ -4,6 +4,7 @@ import { useThemeTokens } from "../theme/ForgeThemeProvider";
|
|
|
4
4
|
import { useAmountFormatter } from "../format/useFormatCurrency";
|
|
5
5
|
import { priceTaxCaption } from "../format/PriceDisplay";
|
|
6
6
|
import { usePricesIncludeTax } from "../../data/queries/useWebsite";
|
|
7
|
+
import { useForgeT } from "../../i18n";
|
|
7
8
|
import { Button } from "./Button";
|
|
8
9
|
import { Loading } from "./Loading";
|
|
9
10
|
|
|
@@ -33,27 +34,31 @@ export function MembershipTiers({
|
|
|
33
34
|
formatAmount,
|
|
34
35
|
checkoutPath = "/i/membership/checkout",
|
|
35
36
|
onSelect,
|
|
36
|
-
heading
|
|
37
|
+
heading,
|
|
37
38
|
className,
|
|
38
39
|
style,
|
|
39
40
|
}: MembershipTiersProps) {
|
|
40
|
-
const
|
|
41
|
+
const tokens = useThemeTokens();
|
|
42
|
+
const t = useForgeT();
|
|
41
43
|
const fmt = useAmountFormatter(formatAmount);
|
|
42
44
|
// Display-only: inclusive stores caption tier prices as tax-inclusive.
|
|
43
45
|
const pricesIncludeTax = usePricesIncludeTax();
|
|
44
46
|
const taxCaption = priceTaxCaption({ pricesIncludeTax });
|
|
45
47
|
const { data: tiers, isLoading } = useGetMembershipTiers();
|
|
48
|
+
// `null` still means "no heading"; anything else falls back to the default.
|
|
49
|
+
const resolvedHeading = heading === null ? null : (heading ?? t("forge.membership_tiers.heading"));
|
|
50
|
+
const freeLabel = t("forge.membership_tiers.price_free");
|
|
46
51
|
|
|
47
52
|
if (isLoading) return <Loading fullPage />;
|
|
48
|
-
if (!tiers?.length) return <p style={{ color:
|
|
53
|
+
if (!tiers?.length) return <p style={{ color: tokens.text }}>{t("forge.membership_tiers.empty_title")}</p>;
|
|
49
54
|
|
|
50
55
|
const priceLabel = (tier: MembershipTier) => {
|
|
51
56
|
if (tier.payWhatYouWant) {
|
|
52
|
-
return
|
|
57
|
+
return t("forge.membership_tiers.price_pwyw", { amount: fmt(tier.payWhatYouWantMinimum ?? 0) });
|
|
53
58
|
}
|
|
54
|
-
if (tier.priceMonthly) return
|
|
55
|
-
if (tier.priceYearly) return
|
|
56
|
-
return
|
|
59
|
+
if (tier.priceMonthly) return t("forge.membership_tiers.price_monthly", { amount: fmt(tier.priceMonthly) });
|
|
60
|
+
if (tier.priceYearly) return t("forge.membership_tiers.price_yearly", { amount: fmt(tier.priceYearly) });
|
|
61
|
+
return freeLabel;
|
|
57
62
|
};
|
|
58
63
|
|
|
59
64
|
const select = (tier: MembershipTier) => {
|
|
@@ -65,8 +70,12 @@ export function MembershipTiers({
|
|
|
65
70
|
};
|
|
66
71
|
|
|
67
72
|
return (
|
|
68
|
-
<div className={className} style={{ color:
|
|
69
|
-
{
|
|
73
|
+
<div className={className} style={{ color: tokens.text, fontFamily: tokens.fontFamily, ...style }}>
|
|
74
|
+
{resolvedHeading && (
|
|
75
|
+
<h1 style={{ fontSize: 32, fontWeight: 800, marginBottom: 24, fontFamily: tokens.headingFontFamily }}>
|
|
76
|
+
{resolvedHeading}
|
|
77
|
+
</h1>
|
|
78
|
+
)}
|
|
70
79
|
<div style={{ display: "flex", flexWrap: "wrap", gap: 20, justifyContent: "center" }}>
|
|
71
80
|
{tiers.map((tier) => (
|
|
72
81
|
<div
|
|
@@ -75,20 +84,20 @@ export function MembershipTiers({
|
|
|
75
84
|
flex: "1 1 260px",
|
|
76
85
|
maxWidth: 340,
|
|
77
86
|
width: "100%",
|
|
78
|
-
background:
|
|
79
|
-
border: `1px solid ${
|
|
80
|
-
borderRadius:
|
|
87
|
+
background: tokens.surface,
|
|
88
|
+
border: `1px solid ${tokens.primary}30`,
|
|
89
|
+
borderRadius: tokens.cornerRadius,
|
|
81
90
|
padding: 24,
|
|
82
91
|
display: "flex",
|
|
83
92
|
flexDirection: "column",
|
|
84
93
|
gap: 12,
|
|
85
94
|
}}
|
|
86
95
|
>
|
|
87
|
-
<h2 style={{ fontSize: 22, fontWeight: 700, fontFamily:
|
|
88
|
-
<div style={{ fontSize: 24, fontWeight: 800, color:
|
|
96
|
+
<h2 style={{ fontSize: 22, fontWeight: 700, fontFamily: tokens.headingFontFamily }}>{tier.name}</h2>
|
|
97
|
+
<div style={{ fontSize: 24, fontWeight: 800, color: tokens.primary }}>
|
|
89
98
|
{priceLabel(tier)}
|
|
90
|
-
{taxCaption && priceLabel(tier) !==
|
|
91
|
-
<span style={{ display: "block", fontSize: 12, fontWeight: 400, color:
|
|
99
|
+
{taxCaption && priceLabel(tier) !== freeLabel && (
|
|
100
|
+
<span style={{ display: "block", fontSize: 12, fontWeight: 400, color: tokens.text, opacity: 0.65 }}>
|
|
92
101
|
{taxCaption}
|
|
93
102
|
</span>
|
|
94
103
|
)}
|
|
@@ -98,14 +107,14 @@ export function MembershipTiers({
|
|
|
98
107
|
<ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 6 }}>
|
|
99
108
|
{tier.benefits.map((b) => (
|
|
100
109
|
<li key={b.id} style={{ display: "flex", gap: 8, opacity: 0.85 }}>
|
|
101
|
-
<span style={{ color:
|
|
110
|
+
<span style={{ color: tokens.primary }}>✓</span>
|
|
102
111
|
{b.title}
|
|
103
112
|
</li>
|
|
104
113
|
))}
|
|
105
114
|
</ul>
|
|
106
115
|
)}
|
|
107
116
|
<Button fullWidth style={{ marginTop: "auto" }} onClick={() => select(tier)}>
|
|
108
|
-
|
|
117
|
+
{t("forge.membership_tiers.select")}
|
|
109
118
|
</Button>
|
|
110
119
|
</div>
|
|
111
120
|
))}
|