@tribe-nest/forge 2.2.0 → 3.4.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/createForgeClient.ts +85 -2
- package/src/client/tokenStorage.ts +31 -0
- package/src/contexts/AppAuthContext.tsx +100 -15
- package/src/contexts/PublicAuthContext.tsx +46 -9
- package/src/data/queries/useCheckouts.ts +84 -1
- package/src/data/queries/useCoachingAvailability.ts +18 -3
- package/src/data/queries/useCourseAccess.ts +1 -1
- package/src/data/queries/useCourses.ts +42 -1
- package/src/data/queries/useEvents.ts +15 -1
- package/src/data/queries/usePaymentFlow.ts +12 -0
- package/src/data/queries/useWebsite.ts +6 -0
- package/src/index.ts +19 -2
- package/src/provider/ForgeProvider.tsx +30 -1
- package/src/server/_tests/platformEvents.spec.ts +315 -0
- package/src/server/index.ts +17 -0
- package/src/server/jobs.ts +41 -10
- package/src/server/platform.ts +234 -9
- package/src/server/platformEvents.generated.ts +422 -0
- package/src/types/models.ts +110 -3
- package/src/ui/headless/auth/useSignupForm.ts +69 -4
- package/src/ui/headless/calendar/useAddToCalendar.ts +194 -0
- package/src/ui/headless/checkout/_tests/bundleCoupon.spec.ts +169 -0
- package/src/ui/headless/checkout/bundleCoupon.ts +96 -0
- package/src/ui/headless/checkout/useCheckout.ts +156 -8
- package/src/ui/headless/coaching/useCoachingBooking.ts +53 -3
- package/src/ui/headless/coupon/_tests/couponFailureMessage.spec.ts +84 -0
- package/src/ui/headless/coupon/useCouponField.ts +164 -0
- package/src/ui/headless/course/useCourseCheckout.ts +113 -18
- package/src/ui/headless/event/useEventCheckout.ts +53 -2
- package/src/ui/headless/index.ts +15 -0
- package/src/ui/headless/work/useWorkPortal.ts +24 -21
- package/src/ui/index.ts +7 -0
- package/src/ui/shell/PoweredBy.tsx +60 -0
- package/src/ui/shell/TribeNestApp.tsx +15 -1
- package/src/ui/shell/shellGating.spec.ts +21 -1
- package/src/ui/shell/shellGating.ts +14 -0
- package/src/ui/styled/AddToCalendar.tsx +104 -0
- package/src/ui/styled/Checkout.tsx +45 -14
- package/src/ui/styled/CoachingBooking.tsx +28 -8
- package/src/ui/styled/CoachingConfirmation.tsx +12 -0
- package/src/ui/styled/CourseCheckout.tsx +49 -18
- package/src/ui/styled/DiscountCode.tsx +206 -0
- package/src/ui/styled/EventConfirmation.tsx +68 -22
- package/src/ui/styled/EventDetail.tsx +18 -5
- package/src/ui/styled/EventTickets.tsx +49 -5
- package/src/ui/styled/SignupForm.tsx +86 -35
- package/src/ui/styled/_tests/DiscountCode.spec.tsx +272 -0
- package/src/ui/styled/_tests/EventConfirmation.spec.tsx +154 -0
- package/src/ui/styled/work/WorkInviteAccept.tsx +54 -5
- package/src/utils/_tests/safeRedirect.spec.ts +117 -0
- package/src/utils/_tests/ticketOrderOutcome.spec.ts +126 -0
- package/src/utils/safeRedirect.ts +41 -0
- package/src/utils/ticketOrderOutcome.ts +125 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AlertTriangle, ArrowRight, Calendar, MapPin, Ticket } from "lucide-react";
|
|
1
|
+
import { AlertTriangle, ArrowRight, Calendar, MapPin, RotateCcw, Ticket } from "lucide-react";
|
|
2
2
|
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
3
3
|
import { readableTextOn } from "../theme/contrast";
|
|
4
4
|
import { useAmountFormatter } from "../format/useFormatCurrency";
|
|
@@ -6,20 +6,8 @@ import { Loading } from "./Loading";
|
|
|
6
6
|
import { ConfirmationStage, ConfirmationCard, CheckSeal, WarnSeal, Perforation, ConfirmationRow, alpha } from "./Confirmation";
|
|
7
7
|
import { useEventOrderFinalize } from "../../data/queries/useFinalize";
|
|
8
8
|
import { useEvent } from "../../data/queries/useEvents";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
// A paid (or free) order advances through fulfillment — processing → shipped /
|
|
12
|
-
// delivered / processed. All of these mean the order succeeded, so the ticket
|
|
13
|
-
// confirmation should treat them as "paid"; only failed/pending-payment states
|
|
14
|
-
// are "not completed". (A free order can be `delivered` by the time this loads.)
|
|
15
|
-
const PAID_STATES = new Set<OrderStatus>([
|
|
16
|
-
OrderStatus.Paid,
|
|
17
|
-
OrderStatus.Processing,
|
|
18
|
-
OrderStatus.LabelPurchased,
|
|
19
|
-
OrderStatus.Shipped,
|
|
20
|
-
OrderStatus.Delivered,
|
|
21
|
-
OrderStatus.Processed,
|
|
22
|
-
]);
|
|
9
|
+
import { AddToCalendar } from "./AddToCalendar";
|
|
10
|
+
import { getTicketOrderOutcome, TICKET_ORDER_REFUNDED_COPY } from "../../utils/ticketOrderOutcome";
|
|
23
11
|
|
|
24
12
|
export interface EventConfirmationProps {
|
|
25
13
|
slug: string;
|
|
@@ -78,7 +66,13 @@ export function EventConfirmation({
|
|
|
78
66
|
);
|
|
79
67
|
}
|
|
80
68
|
|
|
81
|
-
|
|
69
|
+
// `cancelled` means two different things — swept-abandoned and refund-voided —
|
|
70
|
+
// and the copy for one is a lie told to the other. `getTicketOrderOutcome`
|
|
71
|
+
// reads the order's refund columns to tell them apart; the reasoning for why
|
|
72
|
+
// that is a trustworthy signal lives there.
|
|
73
|
+
const outcome = getTicketOrderOutcome(order);
|
|
74
|
+
const paid = outcome === "paid";
|
|
75
|
+
const refunded = outcome === "refunded";
|
|
82
76
|
const fmt = (n: number) => formatAmt(n, order.currency || undefined);
|
|
83
77
|
const location =
|
|
84
78
|
event && event.type !== "virtual" && event.address
|
|
@@ -90,7 +84,14 @@ export function EventConfirmation({
|
|
|
90
84
|
<ConfirmationCard>
|
|
91
85
|
{/* ── Ceremony ── */}
|
|
92
86
|
<div style={{ padding: "38px 30px 26px", textAlign: "center" }}>
|
|
93
|
-
{paid ?
|
|
87
|
+
{paid ? (
|
|
88
|
+
<CheckSeal />
|
|
89
|
+
) : refunded ? (
|
|
90
|
+
// Neutral, not alarmed: a refund is a completed outcome, not a fault.
|
|
91
|
+
<NeutralSeal icon={<RotateCcw size={32} color={alpha(text, 0.65)} />} text={text} />
|
|
92
|
+
) : (
|
|
93
|
+
<WarnSeal icon={<AlertTriangle size={34} color="#f59e0b" />} />
|
|
94
|
+
)}
|
|
94
95
|
|
|
95
96
|
<p
|
|
96
97
|
className="cf-rise"
|
|
@@ -100,19 +101,21 @@ export function EventConfirmation({
|
|
|
100
101
|
fontWeight: 700,
|
|
101
102
|
letterSpacing: "0.22em",
|
|
102
103
|
textTransform: "uppercase",
|
|
103
|
-
color: paid ? primary : "#f59e0b",
|
|
104
|
+
color: paid ? primary : refunded ? alpha(text, 0.65) : "#f59e0b",
|
|
104
105
|
animationDelay: "0.15s",
|
|
105
106
|
}}
|
|
106
107
|
>
|
|
107
|
-
{paid ? "E-Ticket" : "Order " + order.status.replace(/_/g, " ")}
|
|
108
|
+
{paid ? "E-Ticket" : refunded ? TICKET_ORDER_REFUNDED_COPY.eyebrow : "Order " + order.status.replace(/_/g, " ")}
|
|
108
109
|
</p>
|
|
109
110
|
<h1 className="cf-rise" style={{ fontSize: 29, fontWeight: 800, lineHeight: 1.12, margin: "0 0 8px", animationDelay: "0.22s" }}>
|
|
110
|
-
{paid ? "You’re going! 🎟" : "Order not completed"}
|
|
111
|
+
{paid ? "You’re going! 🎟" : refunded ? TICKET_ORDER_REFUNDED_COPY.heading : "Order not completed"}
|
|
111
112
|
</h1>
|
|
112
113
|
<p className="cf-rise" style={{ fontSize: 15, color: alpha(text, 0.7), margin: 0, animationDelay: "0.3s" }}>
|
|
113
114
|
{paid
|
|
114
115
|
? `We’ve sent your tickets to ${order.email}.`
|
|
115
|
-
:
|
|
116
|
+
: refunded
|
|
117
|
+
? TICKET_ORDER_REFUNDED_COPY.body
|
|
118
|
+
: `Your order is ${order.status.replace(/_/g, " ")}. If you were charged, contact support.`}
|
|
116
119
|
</p>
|
|
117
120
|
</div>
|
|
118
121
|
|
|
@@ -134,6 +137,20 @@ export function EventConfirmation({
|
|
|
134
137
|
</>
|
|
135
138
|
)}
|
|
136
139
|
</div>
|
|
140
|
+
|
|
141
|
+
{/* Diarise it while the confirmation is still on screen — the single
|
|
142
|
+
highest-intent moment there is. Not offered for a called-off event. */}
|
|
143
|
+
{paid && event && event.status !== "cancelled" && (
|
|
144
|
+
<AddToCalendar
|
|
145
|
+
variant="compact"
|
|
146
|
+
title={event.title}
|
|
147
|
+
start={event.dateTime}
|
|
148
|
+
end={event.endDateTime}
|
|
149
|
+
description={event.description}
|
|
150
|
+
location={location}
|
|
151
|
+
style={{ marginTop: 12 }}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
137
154
|
</div>
|
|
138
155
|
|
|
139
156
|
<Perforation />
|
|
@@ -173,7 +190,11 @@ export function EventConfirmation({
|
|
|
173
190
|
borderTop: `1px solid ${alpha(text, 0.1)}`,
|
|
174
191
|
}}
|
|
175
192
|
>
|
|
176
|
-
|
|
193
|
+
{/* Never "Total paid" for money that has gone back, and never a bare
|
|
194
|
+
"Total" for money that never moved. */}
|
|
195
|
+
<span style={{ fontWeight: 700 }}>
|
|
196
|
+
{paid ? "Total paid" : refunded ? TICKET_ORDER_REFUNDED_COPY.totalLabel : "Total"}
|
|
197
|
+
</span>
|
|
177
198
|
{/* The exact charged total (charged currency) when present — never a re-converted catalog amount. */}
|
|
178
199
|
<span style={{ fontSize: 18, fontWeight: 800, color: primary }}>
|
|
179
200
|
{order.totalAmountInChargedCurrency
|
|
@@ -219,6 +240,31 @@ export function EventConfirmation({
|
|
|
219
240
|
);
|
|
220
241
|
}
|
|
221
242
|
|
|
243
|
+
/**
|
|
244
|
+
* The seal for a refunded order: same geometry as `CheckSeal`/`WarnSeal`, but
|
|
245
|
+
* drawn in the theme's own text color rather than warning amber. A refund is a
|
|
246
|
+
* settled outcome, not an error, and an amber warning ring says otherwise.
|
|
247
|
+
*/
|
|
248
|
+
function NeutralSeal({ icon, text }: { icon: React.ReactNode; text: string }) {
|
|
249
|
+
return (
|
|
250
|
+
<div
|
|
251
|
+
className="cf-pop"
|
|
252
|
+
style={{
|
|
253
|
+
width: 76,
|
|
254
|
+
height: 76,
|
|
255
|
+
margin: "0 auto",
|
|
256
|
+
borderRadius: "50%",
|
|
257
|
+
display: "grid",
|
|
258
|
+
placeItems: "center",
|
|
259
|
+
background: alpha(text, 0.06),
|
|
260
|
+
border: `1px solid ${alpha(text, 0.18)}`,
|
|
261
|
+
}}
|
|
262
|
+
>
|
|
263
|
+
{icon}
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
222
268
|
function Line({ icon, text, muted }: { icon: React.ReactNode; text: string; muted: string }) {
|
|
223
269
|
return (
|
|
224
270
|
<div style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 14, color: muted, padding: "3px 0" }}>
|
|
@@ -8,6 +8,7 @@ import { usePricesIncludeTax } from "../../data/queries/useWebsite";
|
|
|
8
8
|
import { Loading } from "./Loading";
|
|
9
9
|
import { EventTickets } from "./EventTickets";
|
|
10
10
|
import { EventCountdown } from "./EventCountdown";
|
|
11
|
+
import { AddToCalendar } from "./AddToCalendar";
|
|
11
12
|
|
|
12
13
|
export interface EventDetailProps {
|
|
13
14
|
/** Event slug (resolves the event). */
|
|
@@ -63,6 +64,10 @@ export function EventDetail({
|
|
|
63
64
|
|
|
64
65
|
const cover = event.media?.find((m) => m.type === "image")?.url ?? event.media?.[0]?.url;
|
|
65
66
|
const minPrice = event.tickets.length ? Math.min(...event.tickets.map((t) => t.price)) : 0;
|
|
67
|
+
const locationText =
|
|
68
|
+
event.type === "virtual" || !event.address
|
|
69
|
+
? "Virtual"
|
|
70
|
+
: [event.address.name, event.address.street, event.address.city, event.address.country].filter(Boolean).join(", ");
|
|
66
71
|
|
|
67
72
|
return (
|
|
68
73
|
<div
|
|
@@ -96,14 +101,22 @@ export function EventDetail({
|
|
|
96
101
|
</span>
|
|
97
102
|
<span style={{ display: "flex", alignItems: "flex-start", gap: 8 }}>
|
|
98
103
|
<MapPin size={16} style={{ marginTop: 2, flexShrink: 0 }} />
|
|
99
|
-
{
|
|
100
|
-
? "Virtual"
|
|
101
|
-
: [event.address.name, event.address.street, event.address.city, event.address.country]
|
|
102
|
-
.filter(Boolean)
|
|
103
|
-
.join(", ")}
|
|
104
|
+
{locationText}
|
|
104
105
|
</span>
|
|
105
106
|
</div>
|
|
106
107
|
|
|
108
|
+
{/* Nothing to diarise for an event that has been called off. */}
|
|
109
|
+
{event.status !== "cancelled" && (
|
|
110
|
+
<AddToCalendar
|
|
111
|
+
title={event.title}
|
|
112
|
+
start={event.dateTime}
|
|
113
|
+
end={event.endDateTime}
|
|
114
|
+
description={event.description}
|
|
115
|
+
location={locationText}
|
|
116
|
+
style={{ marginTop: 16 }}
|
|
117
|
+
/>
|
|
118
|
+
)}
|
|
119
|
+
|
|
107
120
|
{event.description && (
|
|
108
121
|
<>
|
|
109
122
|
<h2 style={{ fontSize: 20, fontWeight: 700, margin: "24px 0 8px", fontFamily: t.headingFontFamily }}>About this event</h2>
|
|
@@ -9,6 +9,7 @@ import { usePricesIncludeTax } from "../../data/queries/useWebsite";
|
|
|
9
9
|
import { Loading } from "./Loading";
|
|
10
10
|
import { usePaymentRenderer, type PaymentRenderProps } from "../payment/ForgePaymentProvider";
|
|
11
11
|
import { buttonStyle } from "./Button";
|
|
12
|
+
import { DiscountCodeField, DiscountSummaryLines } from "./DiscountCode";
|
|
12
13
|
import {
|
|
13
14
|
DialogRoot,
|
|
14
15
|
DialogTrigger,
|
|
@@ -420,6 +421,15 @@ function DetailsStep({ c, event, fmt }: { c: Checkout; event: IEvent; fmt: (n: n
|
|
|
420
421
|
);
|
|
421
422
|
})}
|
|
422
423
|
|
|
424
|
+
{/* Tickets have no apply-coupon endpoint, so the code is carried into
|
|
425
|
+
the order that "Continue" creates — hence `deferred`. Its result is
|
|
426
|
+
what draws the discount line on the payment step. */}
|
|
427
|
+
{c.subTotal > 0 && (
|
|
428
|
+
<div style={{ marginTop: 4, paddingTop: 12, borderTop: `1px solid ${a(theme.colors.primary, 0.13)}` }}>
|
|
429
|
+
<DiscountCodeField coupon={c.coupon} fmt={fmt} mode="deferred" />
|
|
430
|
+
</div>
|
|
431
|
+
)}
|
|
432
|
+
|
|
423
433
|
{(localError || c.error) && <p style={{ color: "#ef4444", fontSize: 14 }}>{localError ?? c.error}</p>}
|
|
424
434
|
</div>
|
|
425
435
|
<ActionBar
|
|
@@ -461,6 +471,35 @@ function PaymentStep({
|
|
|
461
471
|
marginBottom: 16,
|
|
462
472
|
}}
|
|
463
473
|
>
|
|
474
|
+
{/* What the code actually did, from the order response — never a total
|
|
475
|
+
that silently shrank. Automatic (no-code) discounts land here too. */}
|
|
476
|
+
<DiscountSummaryLines
|
|
477
|
+
subTotal={c.subTotal}
|
|
478
|
+
discountAmount={c.coupon.discountAmount}
|
|
479
|
+
appliedCoupons={c.coupon.appliedCoupons}
|
|
480
|
+
fmt={fmt}
|
|
481
|
+
/>
|
|
482
|
+
{c.coupon.hasDiscount && (
|
|
483
|
+
<div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 8 }}>
|
|
484
|
+
<button
|
|
485
|
+
type="button"
|
|
486
|
+
data-testid="discount-code-remove"
|
|
487
|
+
onClick={c.clearCoupon}
|
|
488
|
+
style={{
|
|
489
|
+
background: "transparent",
|
|
490
|
+
border: "none",
|
|
491
|
+
padding: 0,
|
|
492
|
+
fontSize: 13,
|
|
493
|
+
textDecoration: "underline",
|
|
494
|
+
cursor: "pointer",
|
|
495
|
+
color: theme.colors.text,
|
|
496
|
+
fontFamily: theme.fontFamily,
|
|
497
|
+
}}
|
|
498
|
+
>
|
|
499
|
+
Remove discount
|
|
500
|
+
</button>
|
|
501
|
+
</div>
|
|
502
|
+
)}
|
|
464
503
|
{taxSummary?.mode === "exclusive" && (
|
|
465
504
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
|
|
466
505
|
<span style={{ fontSize: 14, opacity: 0.75 }}>Tax</span>
|
|
@@ -525,13 +564,18 @@ function ActionBar({
|
|
|
525
564
|
border: `1px solid ${a(theme.colors.primary, 0.25)}`,
|
|
526
565
|
borderRadius: theme.cornerRadius,
|
|
527
566
|
marginBottom: 16,
|
|
528
|
-
display: "flex",
|
|
529
|
-
justifyContent: "space-between",
|
|
530
|
-
alignItems: "center",
|
|
531
567
|
}}
|
|
532
568
|
>
|
|
533
|
-
<
|
|
534
|
-
|
|
569
|
+
<DiscountSummaryLines
|
|
570
|
+
subTotal={c.subTotal}
|
|
571
|
+
discountAmount={c.coupon.discountAmount}
|
|
572
|
+
appliedCoupons={c.coupon.appliedCoupons}
|
|
573
|
+
fmt={fmt}
|
|
574
|
+
/>
|
|
575
|
+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
576
|
+
<span style={{ fontWeight: 600 }}>Total</span>
|
|
577
|
+
<span style={{ fontSize: 18, fontWeight: 700, color: theme.colors.primary }}>{fmt(c.totalAmount)}</span>
|
|
578
|
+
</div>
|
|
535
579
|
</div>
|
|
536
580
|
<div style={{ display: "flex", gap: 8, justifyContent: "space-between" }}>
|
|
537
581
|
{secondary ? (
|
|
@@ -52,51 +52,102 @@ export function SignupForm({ onSuccess, loginHref, membershipTierId, couponCode
|
|
|
52
52
|
|
|
53
53
|
return (
|
|
54
54
|
<div style={card}>
|
|
55
|
-
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 24 }}>
|
|
55
|
+
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 24 }}>
|
|
56
|
+
{form.awaitingCode ? "Check your email" : "Create your account"}
|
|
57
|
+
</h1>
|
|
56
58
|
{form.error && <p style={{ color: "#ef4444", marginBottom: 16 }}>{form.error}</p>}
|
|
57
59
|
<form
|
|
58
60
|
onSubmit={(e) => {
|
|
59
61
|
e.preventDefault();
|
|
60
|
-
|
|
62
|
+
// Signup is two steps: create, then redeem the emailed code.
|
|
63
|
+
if (form.awaitingCode) form.submitCode();
|
|
64
|
+
else form.submit();
|
|
61
65
|
}}
|
|
62
66
|
style={{ display: "flex", flexDirection: "column", gap: 16 }}
|
|
63
67
|
>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
68
|
+
{form.awaitingCode ? (
|
|
69
|
+
<>
|
|
70
|
+
<p style={{ fontSize: 14, margin: 0 }}>
|
|
71
|
+
We emailed a 6-digit code to <strong>{form.email}</strong>. Enter it to finish creating your account.
|
|
72
|
+
</p>
|
|
73
|
+
{field(
|
|
74
|
+
"Verification code",
|
|
75
|
+
<input
|
|
76
|
+
inputMode="numeric"
|
|
77
|
+
autoComplete="one-time-code"
|
|
78
|
+
value={form.code}
|
|
79
|
+
onChange={(e) => form.setCode(e.target.value)}
|
|
80
|
+
style={input}
|
|
81
|
+
placeholder="123456"
|
|
82
|
+
/>,
|
|
83
|
+
)}
|
|
84
|
+
</>
|
|
85
|
+
) : (
|
|
86
|
+
<>
|
|
87
|
+
<div style={{ display: "flex", gap: 12 }}>
|
|
88
|
+
{field(
|
|
89
|
+
"First name",
|
|
90
|
+
<input
|
|
91
|
+
value={form.firstName}
|
|
92
|
+
onChange={(e) => form.setFirstName(e.target.value)}
|
|
93
|
+
style={input}
|
|
94
|
+
placeholder="First name"
|
|
95
|
+
/>,
|
|
96
|
+
)}
|
|
97
|
+
{field(
|
|
98
|
+
"Last name",
|
|
99
|
+
<input
|
|
100
|
+
value={form.lastName}
|
|
101
|
+
onChange={(e) => form.setLastName(e.target.value)}
|
|
102
|
+
style={input}
|
|
103
|
+
placeholder="Last name"
|
|
104
|
+
/>,
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
{field(
|
|
108
|
+
"Email",
|
|
109
|
+
<input
|
|
110
|
+
type="email"
|
|
111
|
+
value={form.email}
|
|
112
|
+
onChange={(e) => form.setEmail(e.target.value)}
|
|
113
|
+
style={input}
|
|
114
|
+
placeholder="Email"
|
|
115
|
+
/>,
|
|
116
|
+
)}
|
|
117
|
+
{field(
|
|
118
|
+
"Password",
|
|
119
|
+
<input
|
|
120
|
+
type="password"
|
|
121
|
+
value={form.password}
|
|
122
|
+
onChange={(e) => form.setPassword(e.target.value)}
|
|
123
|
+
style={input}
|
|
124
|
+
placeholder="At least 8 characters"
|
|
125
|
+
/>,
|
|
126
|
+
)}
|
|
127
|
+
<label style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13 }}>
|
|
128
|
+
<input
|
|
129
|
+
type="checkbox"
|
|
130
|
+
checked={form.acceptedTerms}
|
|
131
|
+
onChange={(e) => form.setAcceptedTerms(e.target.checked)}
|
|
132
|
+
style={{ accentColor: theme.colors.primary }}
|
|
133
|
+
/>
|
|
134
|
+
I accept the Terms and Privacy Policy
|
|
135
|
+
</label>
|
|
136
|
+
</>
|
|
77
137
|
)}
|
|
78
|
-
{field(
|
|
79
|
-
"Password",
|
|
80
|
-
<input
|
|
81
|
-
type="password"
|
|
82
|
-
value={form.password}
|
|
83
|
-
onChange={(e) => form.setPassword(e.target.value)}
|
|
84
|
-
style={input}
|
|
85
|
-
placeholder="At least 8 characters"
|
|
86
|
-
/>,
|
|
87
|
-
)}
|
|
88
|
-
<label style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13 }}>
|
|
89
|
-
<input
|
|
90
|
-
type="checkbox"
|
|
91
|
-
checked={form.acceptedTerms}
|
|
92
|
-
onChange={(e) => form.setAcceptedTerms(e.target.checked)}
|
|
93
|
-
style={{ accentColor: theme.colors.primary }}
|
|
94
|
-
/>
|
|
95
|
-
I accept the Terms and Privacy Policy
|
|
96
|
-
</label>
|
|
97
138
|
<button type="submit" disabled={form.isSubmitting} style={button}>
|
|
98
|
-
{form.isSubmitting ? "
|
|
139
|
+
{form.isSubmitting ? "Working…" : form.awaitingCode ? "Verify and continue" : "Sign up"}
|
|
99
140
|
</button>
|
|
141
|
+
{form.awaitingCode && (
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
onClick={() => form.resendCode()}
|
|
145
|
+
disabled={form.isSubmitting}
|
|
146
|
+
style={{ background: "none", border: "none", color: theme.colors.primary, cursor: "pointer", fontSize: 13 }}
|
|
147
|
+
>
|
|
148
|
+
Resend code
|
|
149
|
+
</button>
|
|
150
|
+
)}
|
|
100
151
|
</form>
|
|
101
152
|
<p style={{ marginTop: 16, fontSize: 14, textAlign: "center" }}>
|
|
102
153
|
Already have an account?{" "}
|