@tribe-nest/forge 3.48.0 → 3.52.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.
Files changed (38) hide show
  1. package/package.json +2 -2
  2. package/src/data/queries/useCohortEnrolment.ts +87 -0
  3. package/src/data/queries/useMembershipCalls.ts +58 -8
  4. package/src/i18n/de.json +46 -0
  5. package/src/i18n/en.json +46 -0
  6. package/src/index.ts +1 -0
  7. package/src/types/models.ts +78 -5
  8. package/src/ui/headless/cohort/useCohortCheckout.ts +180 -0
  9. package/src/ui/headless/memberHome/useMemberCalls.ts +13 -0
  10. package/src/ui/index.ts +10 -0
  11. package/src/ui/styled/BlogPost.tsx +1 -1
  12. package/src/ui/styled/ChatRoom.tsx +2 -2
  13. package/src/ui/styled/Checkout.tsx +125 -125
  14. package/src/ui/styled/CoachingBooking.tsx +1 -1
  15. package/src/ui/styled/CoachingDetail.tsx +4 -4
  16. package/src/ui/styled/CohortCheckout.tsx +450 -0
  17. package/src/ui/styled/CohortConfirmation.tsx +259 -0
  18. package/src/ui/styled/CohortPage.tsx +31 -1
  19. package/src/ui/styled/CourseAccess.tsx +19 -6
  20. package/src/ui/styled/CourseCallsSection.tsx +99 -0
  21. package/src/ui/styled/CourseDetail.tsx +4 -4
  22. package/src/ui/styled/EventDetail.tsx +4 -4
  23. package/src/ui/styled/FormRenderer.tsx +14 -4
  24. package/src/ui/styled/PaystackPayButton.tsx +1 -1
  25. package/src/ui/styled/PodcastEpisode.tsx +1 -1
  26. package/src/ui/styled/ProductDetail.tsx +4 -4
  27. package/src/ui/styled/_tests/CohortCheckout.spec.tsx +232 -0
  28. package/src/ui/styled/_tests/CourseCallsSection.spec.tsx +163 -0
  29. package/src/ui/styled/_tests/packagedCssIsCurrent.spec.ts +43 -0
  30. package/src/ui/styled/_tests/utilityPrefix.spec.ts +156 -0
  31. package/src/ui/styled/broadcast/BroadcastMoqStage.tsx +1 -1
  32. package/src/ui/styled/chat/ChatThreadPanel.tsx +1 -1
  33. package/src/ui/styled/forge-utilities.css +463 -731
  34. package/src/ui/styled/members/MemberCallsCalendar.tsx +21 -3
  35. package/src/ui/styled/members/MemberHomeLayout.tsx +4 -4
  36. package/src/ui/styled/members/MemberPostsFeed.tsx +1 -1
  37. package/src/ui/styled/members/PostCollectionsGrid.tsx +1 -1
  38. package/src/ui/styled/musicLinkStyles.tsx +4 -4
@@ -352,7 +352,7 @@ function SlotStep({ c, product, fmt }: { c: Booking; product: CoachingProduct; f
352
352
  {dayName}, {dateString}
353
353
  </h3>
354
354
  </div>
355
- <div className="grid grid-cols-2 md:grid-cols-4" style={{ gap: 10 }}>
355
+ <div className="fg:grid fg:grid-cols-2 fg:md:grid-cols-4" style={{ gap: 10 }}>
356
356
  {daySlots.map((slot) => {
357
357
  const selected = slot.slotId === c.selectedSlotId;
358
358
  return (
@@ -50,7 +50,7 @@ export function CoachingDetail({
50
50
 
51
51
  return (
52
52
  <div
53
- className={`pb-32 md:pb-0${className ? ` ${className}` : ""}`}
53
+ className={`fg:pb-32 fg:md:pb-0${className ? ` ${className}` : ""}`}
54
54
  style={{ maxWidth: 1040, margin: "0 auto", color: tokens.text, ...style }}
55
55
  >
56
56
  {cover && (
@@ -61,8 +61,8 @@ export function CoachingDetail({
61
61
  />
62
62
  )}
63
63
 
64
- <div className="flex flex-col md:flex-row md:items-start gap-6 mt-6">
65
- <div className="flex-1 min-w-0">
64
+ <div className="fg:flex fg:flex-col fg:md:flex-row fg:md:items-start fg:gap-6 fg:mt-6">
65
+ <div className="fg:flex-1 fg:min-w-0">
66
66
  <h1 style={{ fontSize: 32, fontWeight: 800, margin: "0 0 12px", fontFamily: tokens.headingFontFamily }}>{product.title}</h1>
67
67
 
68
68
  <div style={{ display: "flex", flexDirection: "column", gap: 8, fontSize: 14 }}>
@@ -82,7 +82,7 @@ export function CoachingDetail({
82
82
  </div>
83
83
 
84
84
  <aside
85
- className="fixed bottom-0 left-0 right-0 z-40 w-full md:sticky md:top-4 md:bottom-auto md:left-auto md:right-auto md:w-[300px] md:shrink-0 flex flex-col gap-3"
85
+ className="fg:fixed fg:bottom-0 fg:left-0 fg:right-0 fg:z-40 fg:w-full fg:md:sticky fg:md:top-4 fg:md:bottom-auto fg:md:left-auto fg:md:right-auto fg:md:w-[300px] fg:md:shrink-0 fg:flex fg:flex-col fg:gap-3"
86
86
  style={{
87
87
  padding: 16,
88
88
  border: `1px solid ${tokens.primary}80`,
@@ -0,0 +1,450 @@
1
+ import { type CSSProperties, type ReactNode, useState } from "react";
2
+ import { X } from "lucide-react";
3
+ import { useCohortCheckout } from "../headless/cohort/useCohortCheckout";
4
+ import { useForgeT } from "../../i18n";
5
+ import { useForgeTheme, type ForgeTheme } from "../theme/ForgeThemeProvider";
6
+ import { useAmountFormatter } from "../format/useFormatCurrency";
7
+ import { summarizeTaxQuote } from "../format/PriceDisplay";
8
+ import { Loading } from "./Loading";
9
+ import { PaystackPayButton } from "./PaystackPayButton";
10
+ import { usePaymentRenderer, type PaymentRenderProps } from "../payment/ForgePaymentProvider";
11
+ import { buttonStyle } from "./Button";
12
+ import {
13
+ DialogRoot,
14
+ DialogTrigger,
15
+ DialogPortal,
16
+ DialogOverlay,
17
+ DialogContent,
18
+ DialogTitle,
19
+ DialogClose,
20
+ } from "../headless/dialog";
21
+
22
+ export interface CohortCheckoutProps {
23
+ /** The cohort being sold. */
24
+ cohortId?: string;
25
+ /** Trigger button label. */
26
+ triggerText?: string;
27
+ /** Format a numeric amount for display. */
28
+ formatAmount?: (amount: number) => string;
29
+ /** Optional payment renderer - falls back to a registered <ForgePaymentProvider>. */
30
+ renderPayment?: (props: PaymentRenderProps) => ReactNode;
31
+ /**
32
+ * Stripe return URL for a PAID enrolment. Default
33
+ * `/i/cohorts/:cohortId/finalise`.
34
+ *
35
+ * Put ONLY the bookingId in this URL. The booking's secret is handled for you
36
+ * by the hook, which stashes it in sessionStorage for the leg after the
37
+ * redirect. Putting it in the URL would hand it to history, referrers and
38
+ * shared links.
39
+ */
40
+ finalisePath?: (cohortId: string, bookingId: string) => string;
41
+ /** Host-owned navigation on FREE completion (e.g. router navigate). */
42
+ onComplete?: (info: { cohortId: string; bookingId: string }) => void;
43
+ }
44
+
45
+ /** Append an alpha to a 6-digit hex color (mirrors frontend-shared addAlphaToHexCode). */
46
+ const a = (hex: string, alpha: number) =>
47
+ hex +
48
+ Math.round(alpha * 255)
49
+ .toString(16)
50
+ .padStart(2, "0");
51
+
52
+ /**
53
+ * Themed cohort enrolment: an "Enrol" trigger and a modal running plan choice,
54
+ * buyer details, then payment, on `useCohortCheckout`.
55
+ *
56
+ * THE MISSING HALF. The cohort checkout has existed on the backend
57
+ * (`/public/cohorts/:id/enrollments`) with nothing on any storefront calling it,
58
+ * so a creator could build a cohort, price it, publish it, and have no way to
59
+ * sell a seat. The shared `PricingPlans` block drew the prices as text with no
60
+ * action behind them.
61
+ *
62
+ * WHY PLAN CHOICE IS ITS OWN STEP. A cohort is priced from its plans, so "pay in
63
+ * full or three instalments" is the decision the buyer came to make. It is
64
+ * shown first, with the creator's default preselected.
65
+ */
66
+ export function CohortCheckout({
67
+ cohortId,
68
+ triggerText,
69
+ formatAmount,
70
+ renderPayment,
71
+ finalisePath,
72
+ onComplete,
73
+ }: CohortCheckoutProps) {
74
+ const t = useForgeT();
75
+ const theme = useForgeTheme();
76
+ const registered = usePaymentRenderer();
77
+ const fmt = useAmountFormatter(formatAmount);
78
+ const c = useCohortCheckout(cohortId, { finalisePath, onComplete });
79
+ const renderPay = renderPayment ?? registered ?? undefined;
80
+ const [open, setOpen] = useState(false);
81
+
82
+ const tryClose = (next: boolean) => {
83
+ if (!next && c.step === "payment" && typeof window !== "undefined") {
84
+ if (!window.confirm(t("forge.cohort_checkout.confirm_leave"))) return;
85
+ }
86
+ setOpen(next);
87
+ };
88
+
89
+ // Nothing to sell is not an error, it is a cohort with no plans yet. Drawing a
90
+ // dead "Enrol" button would be worse than drawing nothing.
91
+ if (!c.isLoading && c.plans.length === 0) return null;
92
+
93
+ return (
94
+ <DialogRoot open={open} onOpenChange={tryClose}>
95
+ <DialogTrigger asChild>
96
+ <button style={triggerButtonStyle(theme)}>{triggerText ?? t("forge.cohort_checkout.trigger")}</button>
97
+ </DialogTrigger>
98
+ <DialogPortal>
99
+ <DialogOverlay style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)", zIndex: 50 }} />
100
+ <DialogContent
101
+ style={{
102
+ position: "fixed",
103
+ top: "50%",
104
+ left: "50%",
105
+ transform: "translate(-50%, -50%)",
106
+ width: "min(640px, calc(100vw - 24px))",
107
+ maxHeight: "88vh",
108
+ overflowY: "auto",
109
+ background: theme.colors.background,
110
+ color: theme.colors.text,
111
+ borderRadius: theme.cornerRadius,
112
+ padding: 24,
113
+ zIndex: 51,
114
+ fontFamily: theme.fontFamily,
115
+ }}
116
+ aria-describedby={undefined}
117
+ >
118
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
119
+ <DialogTitle style={{ fontSize: 18, fontWeight: 700, margin: 0 }}>
120
+ {c.cohort?.title ?? c.course?.title ?? t("forge.cohort_checkout.title")}
121
+ </DialogTitle>
122
+ <DialogClose asChild>
123
+ <button aria-label={t("forge.cohort_checkout.aria_close")} style={closeButtonStyle(theme)}>
124
+ <X size={18} />
125
+ </button>
126
+ </DialogClose>
127
+ </div>
128
+
129
+ {c.isLoading && <Loading />}
130
+ {!c.isLoading && c.plans.length === 0 && (
131
+ <p style={{ marginTop: 8 }}>{t("forge.cohort_checkout.no_plans")}</p>
132
+ )}
133
+ {!c.isLoading && c.plans.length > 0 && <Body c={c} fmt={fmt} renderPayment={renderPay} />}
134
+ </DialogContent>
135
+ </DialogPortal>
136
+ </DialogRoot>
137
+ );
138
+ }
139
+
140
+ type Checkout = ReturnType<typeof useCohortCheckout>;
141
+
142
+ function Body({
143
+ c,
144
+ fmt,
145
+ renderPayment,
146
+ }: {
147
+ c: Checkout;
148
+ fmt: (n: number) => string;
149
+ renderPayment?: (props: PaymentRenderProps) => ReactNode;
150
+ }) {
151
+ const stepIndex = c.step === "plan" ? 0 : c.step === "details" ? 1 : 2;
152
+ return (
153
+ <div style={{ width: "100%" }}>
154
+ <Progress current={stepIndex} total={3} />
155
+ {c.step === "plan" && <PlanStep c={c} fmt={fmt} />}
156
+ {c.step === "details" && <DetailsStep c={c} fmt={fmt} />}
157
+ {c.step === "payment" && <PaymentStep c={c} fmt={fmt} renderPayment={renderPayment} />}
158
+ </div>
159
+ );
160
+ }
161
+
162
+ function Progress({ current, total }: { current: number; total: number }) {
163
+ const theme = useForgeTheme();
164
+ const steps = Array.from({ length: total }, (_, i) => i);
165
+ return (
166
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 24 }}>
167
+ {steps.map((s, index) => (
168
+ <div key={s} style={{ display: "flex", alignItems: "center", flex: index < steps.length - 1 ? 1 : "0 0 auto" }}>
169
+ <div
170
+ style={{
171
+ width: 32,
172
+ height: 32,
173
+ borderRadius: "9999px",
174
+ display: "flex",
175
+ alignItems: "center",
176
+ justifyContent: "center",
177
+ fontSize: 14,
178
+ fontWeight: 600,
179
+ border: `1px solid ${index <= current ? theme.colors.primary : a(theme.colors.primary, 0.25)}`,
180
+ background: index <= current ? theme.colors.primary : "transparent",
181
+ color: index <= current ? "#fff" : theme.colors.primary,
182
+ }}
183
+ >
184
+ {index + 1}
185
+ </div>
186
+ {index < steps.length - 1 && (
187
+ <div
188
+ style={{
189
+ flex: 1,
190
+ height: 2,
191
+ margin: "0 16px",
192
+ background: index < current ? theme.colors.primary : a(theme.colors.primary, 0.13),
193
+ }}
194
+ />
195
+ )}
196
+ </div>
197
+ ))}
198
+ </div>
199
+ );
200
+ }
201
+
202
+ /** One line of plan copy: "3 payments of $200, every 30 days" or "Pay in full". */
203
+ function planTerms(
204
+ t: ReturnType<typeof useForgeT>,
205
+ plan: Checkout["plans"][number],
206
+ fmt: (n: number) => string,
207
+ ): string {
208
+ const total = Number(plan.totalAmount);
209
+ if (plan.installmentCount > 1) {
210
+ return t("forge.cohort_checkout.plan_installments", {
211
+ count: String(plan.installmentCount),
212
+ amount: fmt(total / plan.installmentCount),
213
+ days: String(plan.intervalDays),
214
+ });
215
+ }
216
+ return t("forge.cohort_checkout.plan_in_full");
217
+ }
218
+
219
+ // ── Step 1: which plan ────────────────────────────────────────────────────────
220
+ function PlanStep({ c, fmt }: { c: Checkout; fmt: (n: number) => string }) {
221
+ const t = useForgeT();
222
+ const theme = useForgeTheme();
223
+ return (
224
+ <div>
225
+ <h2 style={{ fontSize: 22, fontWeight: 700, marginBottom: 8 }}>{t("forge.cohort_checkout.plan_title")}</h2>
226
+ <p style={{ fontSize: 14, opacity: 0.7, marginBottom: 16 }}>{t("forge.cohort_checkout.plan_subtitle")}</p>
227
+ <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
228
+ {c.plans.map((plan) => {
229
+ const selected = plan.id === c.planId;
230
+ return (
231
+ <button
232
+ key={plan.id}
233
+ type="button"
234
+ onClick={() => c.choosePlan(plan.id)}
235
+ style={{
236
+ textAlign: "left",
237
+ padding: 16,
238
+ cursor: "pointer",
239
+ border: `${selected ? 2 : 1}px solid ${
240
+ selected ? theme.colors.primary : a(theme.colors.primary, 0.25)
241
+ }`,
242
+ borderRadius: theme.cornerRadius,
243
+ background: "transparent",
244
+ color: theme.colors.text,
245
+ fontFamily: theme.fontFamily,
246
+ }}
247
+ >
248
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 12 }}>
249
+ <span style={{ fontWeight: 600 }}>{plan.title}</span>
250
+ <span style={{ fontWeight: 700, color: theme.colors.primary }}>{fmt(Number(plan.totalAmount))}</span>
251
+ </div>
252
+ <div style={{ fontSize: 13, opacity: 0.7, marginTop: 4 }}>{planTerms(t, plan, fmt)}</div>
253
+ </button>
254
+ );
255
+ })}
256
+ </div>
257
+ </div>
258
+ );
259
+ }
260
+
261
+ // ── Step 2: buyer details ─────────────────────────────────────────────────────
262
+ function DetailsStep({ c, fmt }: { c: Checkout; fmt: (n: number) => string }) {
263
+ const t = useForgeT();
264
+ const theme = useForgeTheme();
265
+ const plan = c.selectedPlan;
266
+ const total = plan ? Number(plan.totalAmount) : 0;
267
+
268
+ return (
269
+ <div>
270
+ <h2 style={{ fontSize: 22, fontWeight: 700, marginBottom: 8 }}>{t("forge.cohort_checkout.details_title")}</h2>
271
+ <p style={{ fontSize: 14, opacity: 0.7, marginBottom: 16 }}>{t("forge.cohort_checkout.details_subtitle")}</p>
272
+
273
+ <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
274
+ <div style={{ display: "flex", gap: 12 }}>
275
+ <input
276
+ style={inputStyle(theme)}
277
+ placeholder={t("forge.cohort_checkout.first_name_placeholder")}
278
+ value={c.firstName}
279
+ onChange={(e) => c.setFirstName(e.target.value)}
280
+ />
281
+ <input
282
+ style={inputStyle(theme)}
283
+ placeholder={t("forge.cohort_checkout.last_name_placeholder")}
284
+ value={c.lastName}
285
+ onChange={(e) => c.setLastName(e.target.value)}
286
+ />
287
+ </div>
288
+ <input
289
+ style={inputStyle(theme)}
290
+ type="email"
291
+ placeholder={t("forge.cohort_checkout.email_placeholder")}
292
+ value={c.email}
293
+ onChange={(e) => c.setEmail(e.target.value)}
294
+ />
295
+ </div>
296
+
297
+ {c.error && <p style={{ color: "#ef4444", fontSize: 13, marginTop: 8 }}>{c.error}</p>}
298
+
299
+ <div
300
+ style={{
301
+ marginTop: 20,
302
+ padding: 16,
303
+ border: `2px solid ${a(theme.colors.primary, 0.25)}`,
304
+ borderRadius: theme.cornerRadius,
305
+ }}
306
+ >
307
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
308
+ <span style={{ fontSize: 16 }}>{plan?.title}</span>
309
+ <span style={{ fontSize: 22, fontWeight: 700, color: theme.colors.primary }}>{fmt(total)}</span>
310
+ </div>
311
+ {plan && plan.installmentCount > 1 && (
312
+ <div style={{ fontSize: 13, opacity: 0.75, marginTop: 6 }}>{planTerms(t, plan, fmt)}</div>
313
+ )}
314
+ </div>
315
+
316
+ <div style={{ display: "flex", gap: 12, marginTop: 20 }}>
317
+ <button onClick={() => c.setStep("plan")} style={secondaryButtonStyle(theme)}>
318
+ {t("forge.cohort_checkout.back")}
319
+ </button>
320
+ <button onClick={() => void c.enrol()} disabled={c.isProcessing} style={primaryButtonStyle(theme)}>
321
+ {c.isProcessing ? t("forge.cohort_checkout.working") : t("forge.cohort_checkout.continue_to_payment")}
322
+ </button>
323
+ </div>
324
+ </div>
325
+ );
326
+ }
327
+
328
+ // ── Step 3: payment ───────────────────────────────────────────────────────────
329
+ function PaymentStep({
330
+ c,
331
+ fmt,
332
+ renderPayment,
333
+ }: {
334
+ c: Checkout;
335
+ fmt: (n: number) => string;
336
+ renderPayment?: (props: PaymentRenderProps) => ReactNode;
337
+ }) {
338
+ const t = useForgeT();
339
+ const theme = useForgeTheme();
340
+ /**
341
+ * WHAT IS CHARGED NOW, not what the plan costs. On a three-payment plan the
342
+ * total is what they commit to and the instalment is what leaves their card
343
+ * today, and showing only the total tells them they are about to be charged
344
+ * three times too much.
345
+ */
346
+ const dueNow = c.installmentAmount ?? c.totalAmount ?? 0;
347
+ const total = c.totalAmount ?? dueNow;
348
+ const isInstalments = (c.installmentCount ?? 1) > 1;
349
+ const taxSummary = summarizeTaxQuote(c.taxQuote);
350
+
351
+ return (
352
+ <div>
353
+ <h2 style={{ fontSize: 22, fontWeight: 700, marginBottom: 8 }}>{t("forge.cohort_checkout.payment_title")}</h2>
354
+ <p style={{ fontSize: 14, opacity: 0.7, marginBottom: 16 }}>{t("forge.cohort_checkout.payment_subtitle")}</p>
355
+
356
+ <div
357
+ style={{
358
+ padding: 20,
359
+ border: `2px solid ${a(theme.colors.primary, 0.25)}`,
360
+ borderRadius: theme.cornerRadius,
361
+ marginBottom: 16,
362
+ }}
363
+ >
364
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
365
+ <span style={{ fontSize: 16 }}>{t("forge.cohort_checkout.due_now")}</span>
366
+ <span style={{ fontSize: 22, fontWeight: 700, color: theme.colors.primary }}>
367
+ {fmt(dueNow)}
368
+ {taxSummary && (
369
+ <span
370
+ style={{ display: "block", fontSize: 12, fontWeight: 400, color: theme.colors.text, opacity: 0.65 }}
371
+ >
372
+ {taxSummary.mode === "inclusive"
373
+ ? taxSummary.label
374
+ : t("forge.cohort_checkout.tax_added", { amount: taxSummary.formattedTax })}
375
+ </span>
376
+ )}
377
+ </span>
378
+ </div>
379
+ {isInstalments && (
380
+ <p style={{ fontSize: 13, opacity: 0.75, marginTop: 8 }}>
381
+ {t("forge.cohort_checkout.installment_note", {
382
+ count: String(c.installmentCount),
383
+ total: fmt(total),
384
+ })}
385
+ </p>
386
+ )}
387
+ </div>
388
+
389
+ {/*
390
+ The SECOND consent, on an instalment plan: agreeing to be charged again
391
+ with nobody present. Stripe sometimes needs it confirmed in the browser,
392
+ and a checkout that skips it takes payment one and silently fails the
393
+ rest.
394
+ */}
395
+ {c.setupRequiresAction && c.setupClientSecret && renderPayment && (
396
+ <div style={{ marginBottom: 16 }}>
397
+ <p style={{ fontSize: 13, opacity: 0.8, marginBottom: 8 }}>
398
+ {t("forge.cohort_checkout.authorise_future_payments")}
399
+ </p>
400
+ {renderPayment({ clientSecret: c.setupClientSecret, returnUrl: c.returnUrl, amount: 0, mode: "redirect" })}
401
+ </div>
402
+ )}
403
+
404
+ {c.isPaystack ? (
405
+ c.canOpenPaystack ? (
406
+ <PaystackPayButton
407
+ onPay={c.openPaystackCheckout}
408
+ label={t("forge.cohort_checkout.pay", { amount: fmt(dueNow) })}
409
+ />
410
+ ) : (
411
+ <p style={{ opacity: 0.7 }}>{t("forge.cohort_checkout.preparing_payment")}</p>
412
+ )
413
+ ) : !renderPayment ? (
414
+ <p style={{ color: "#ef4444", fontSize: 14 }}>{t("forge.cohort_checkout.payment_not_configured")}</p>
415
+ ) : c.clientSecret ? (
416
+ renderPayment({ clientSecret: c.clientSecret, returnUrl: c.returnUrl, amount: dueNow, mode: "redirect" })
417
+ ) : (
418
+ <p style={{ opacity: 0.7 }}>{t("forge.cohort_checkout.preparing_payment")}</p>
419
+ )}
420
+
421
+ <button onClick={() => c.setStep("details")} style={{ ...secondaryButtonStyle(theme), marginTop: 16 }}>
422
+ {t("forge.cohort_checkout.back")}
423
+ </button>
424
+ </div>
425
+ );
426
+ }
427
+
428
+ const triggerButtonStyle = (t: ForgeTheme): CSSProperties => buttonStyle(t, { variant: "primary", fullWidth: true });
429
+ const primaryButtonStyle = (t: ForgeTheme): CSSProperties => buttonStyle(t, { variant: "primary" });
430
+ const secondaryButtonStyle = (t: ForgeTheme): CSSProperties => buttonStyle(t, { variant: "outline" });
431
+
432
+ const closeButtonStyle = (t: ForgeTheme): CSSProperties => ({
433
+ background: "transparent",
434
+ border: "none",
435
+ color: t.colors.text,
436
+ cursor: "pointer",
437
+ fontSize: 16,
438
+ lineHeight: 1,
439
+ opacity: 0.7,
440
+ });
441
+
442
+ const inputStyle = (t: ForgeTheme): CSSProperties => ({
443
+ width: "100%",
444
+ padding: 10,
445
+ border: `1px solid ${a(t.colors.primary, 0.25)}`,
446
+ borderRadius: t.cornerRadius,
447
+ background: t.colors.background,
448
+ color: t.colors.text,
449
+ fontFamily: t.fontFamily,
450
+ });