@tribe-nest/forge 3.53.0 → 3.57.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/contexts/AppAuthContext.tsx +3 -2
- package/src/contexts/AudioPlayerContext.tsx +3 -2
- package/src/contexts/CartContext.tsx +3 -2
- package/src/contexts/PublicAuthContext.tsx +3 -2
- package/src/data/queries/useCoachingProducts.ts +15 -1
- package/src/data/queries/useCourses.ts +15 -1
- package/src/data/queries/useEvents.ts +26 -1
- package/src/data/queries/useMembership.ts +176 -4
- package/src/data/queries/useMyTickets.ts +49 -0
- package/src/data/queries/usePaymentFlow.ts +8 -0
- package/src/data/queries/useProducts.ts +15 -1
- package/src/data/queries/useSubscriptions.ts +21 -1
- package/src/i18n/de.json +116 -1
- package/src/i18n/en.json +116 -1
- package/src/i18n/index.ts +3 -2
- package/src/index.ts +22 -0
- package/src/provider/ForgeAppProvider.tsx +8 -1
- package/src/provider/ForgeProvider.tsx +3 -2
- package/src/provider/SiteConfigProvider.tsx +3 -2
- package/src/runtime/RemotePage.tsx +110 -0
- package/src/runtime/hostRuntime.ts +84 -0
- package/src/runtime/pages.spec.ts +41 -0
- package/src/runtime/pages.ts +183 -0
- package/src/runtime/pagesClient.ts +127 -0
- package/src/runtime/registry.spec.ts +45 -0
- package/src/runtime/registry.ts +102 -0
- package/src/server/index.ts +74 -0
- package/src/server/jobs.ts +4 -1
- package/src/server/platformEvents.generated.ts +152 -0
- package/src/types/models.ts +185 -0
- package/src/ui/headless/event/_tests/ticketApproval.spec.ts +137 -0
- package/src/ui/headless/event/_tests/useEventCheckoutApproval.spec.tsx +250 -0
- package/src/ui/headless/event/ticketApproval.ts +109 -0
- package/src/ui/headless/event/useEventCheckout.ts +74 -4
- package/src/ui/headless/index.ts +34 -0
- package/src/ui/headless/membership/_tests/membershipApplication.spec.ts +85 -0
- package/src/ui/headless/membership/_tests/membershipCheckoutRefetch.spec.tsx +2 -0
- package/src/ui/headless/membership/_tests/membershipTrial.spec.ts +61 -0
- package/src/ui/headless/membership/_tests/useMembershipCheckoutApplication.spec.tsx +253 -0
- package/src/ui/headless/membership/_tests/useMembershipCheckoutQuestionnaire.spec.tsx +241 -0
- package/src/ui/headless/membership/_tests/useMembershipCheckoutTrial.spec.tsx +165 -0
- package/src/ui/headless/membership/membershipApplication.ts +109 -0
- package/src/ui/headless/membership/membershipQuestionnaire.ts +44 -0
- package/src/ui/headless/membership/membershipTrial.ts +53 -0
- package/src/ui/headless/membership/useMembershipCheckout.ts +227 -8
- package/src/ui/index.ts +27 -0
- package/src/ui/payment/ForgePaymentProvider.tsx +29 -0
- package/src/ui/payment/ForgeStripePayment.tsx +110 -5
- package/src/ui/payment/_tests/ForgeStripePaymentTrial.spec.tsx +101 -0
- package/src/ui/payment/_tests/stripeConfirmOutcome.spec.ts +61 -0
- package/src/ui/payment/_tests/stripeIntentKind.spec.ts +43 -0
- package/src/ui/payment/stripeConfirmOutcome.ts +50 -0
- package/src/ui/payment/stripeIntentKind.ts +32 -0
- package/src/ui/shell/TribeNestApp.tsx +6 -0
- package/src/ui/styled/AccountDashboard.tsx +239 -15
- package/src/ui/styled/DocumentSigningPage.tsx +516 -0
- package/src/ui/styled/EventConfirmation.tsx +106 -9
- package/src/ui/styled/EventTickets.tsx +107 -13
- package/src/ui/styled/LoginForm.tsx +5 -3
- package/src/ui/styled/MembershipCheckout.tsx +596 -256
- package/src/ui/styled/MembershipTierCallout.tsx +17 -3
- package/src/ui/styled/MembershipTiers.tsx +148 -6
- package/src/ui/styled/ProductGrid.tsx +14 -2
- package/src/ui/styled/ReviewRequestPage.tsx +214 -0
- package/src/ui/styled/SignupForm.tsx +4 -2
- package/src/ui/styled/_tests/AccountDashboardTrial.spec.tsx +105 -0
- package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +106 -0
- package/src/ui/styled/_tests/membershipTiersCuratedAccess.spec.tsx +139 -0
- package/src/ui/styled/_tests/membershipTiersTrial.spec.tsx +97 -0
- package/src/ui/styled/forge-utilities.css +310 -0
- package/src/ui/theme/ForgeThemeProvider.tsx +3 -2
- package/src/utils/_tests/ticketOrderOutcome.spec.ts +82 -0
- package/src/utils/ticketOrderOutcome.ts +57 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AlertTriangle, ArrowRight, Calendar, MapPin, RotateCcw, Ticket } from "lucide-react";
|
|
1
|
+
import { AlertTriangle, ArrowRight, Ban, Calendar, Hourglass, MapPin, RotateCcw, Ticket, TimerOff } from "lucide-react";
|
|
2
2
|
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
3
3
|
import { readableTextOn } from "../theme/contrast";
|
|
4
4
|
import { useAmountFormatter } from "../format/useFormatCurrency";
|
|
@@ -8,7 +8,11 @@ import { useForgeT } from "../../i18n";
|
|
|
8
8
|
import { useEventOrderFinalize } from "../../data/queries/useFinalize";
|
|
9
9
|
import { useEvent } from "../../data/queries/useEvents";
|
|
10
10
|
import { AddToCalendar } from "./AddToCalendar";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
getTicketOrderOutcome,
|
|
13
|
+
ticketRequestFailureKind,
|
|
14
|
+
TICKET_ORDER_REFUNDED_COPY,
|
|
15
|
+
} from "../../utils/ticketOrderOutcome";
|
|
12
16
|
|
|
13
17
|
export interface EventConfirmationProps {
|
|
14
18
|
slug: string;
|
|
@@ -20,6 +24,12 @@ export interface EventConfirmationProps {
|
|
|
20
24
|
formatAmount?: (n: number, currency?: string) => string;
|
|
21
25
|
/** Where the "browse more events" action links. */
|
|
22
26
|
explorePath?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Where a fan whose ticket REQUEST lapsed or could not be settled is sent to
|
|
29
|
+
* try again: the event itself, which may still have seats. Default
|
|
30
|
+
* `/i/events/:slug`.
|
|
31
|
+
*/
|
|
32
|
+
eventPath?: string;
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
/** Locale date-time formatting, replicating the site-starter `formatDateTime`. */
|
|
@@ -42,6 +52,7 @@ export function EventConfirmation({
|
|
|
42
52
|
orderId,
|
|
43
53
|
formatAmount,
|
|
44
54
|
explorePath = "/i/events",
|
|
55
|
+
eventPath,
|
|
45
56
|
}: EventConfirmationProps) {
|
|
46
57
|
const theme = useForgeTheme();
|
|
47
58
|
const t = useForgeT();
|
|
@@ -75,6 +86,15 @@ export function EventConfirmation({
|
|
|
75
86
|
const outcome = getTicketOrderOutcome(order);
|
|
76
87
|
const paid = outcome === "paid";
|
|
77
88
|
const refunded = outcome === "refunded";
|
|
89
|
+
/**
|
|
90
|
+
* Ticket requests (curated access, feature 3). `awaiting` is a request the
|
|
91
|
+
* organiser has not decided; `requestEnded` names how one ended without a
|
|
92
|
+
* sale (declined, expired, or approved but the card could not be settled),
|
|
93
|
+
* and is `null` for an ordinary abandoned checkout, whose copy is unchanged.
|
|
94
|
+
*/
|
|
95
|
+
const awaiting = outcome === "awaiting_approval";
|
|
96
|
+
const requestEnded = ticketRequestFailureKind(order);
|
|
97
|
+
const tryAgainPath = eventPath ?? `/i/events/${slug}`;
|
|
78
98
|
const fmt = (n: number) => formatAmt(n, order.currency || undefined);
|
|
79
99
|
const location =
|
|
80
100
|
event && event.type !== "virtual" && event.address
|
|
@@ -91,6 +111,14 @@ export function EventConfirmation({
|
|
|
91
111
|
) : refunded ? (
|
|
92
112
|
// Neutral, not alarmed: a refund is a completed outcome, not a fault.
|
|
93
113
|
<NeutralSeal icon={<RotateCcw size={32} color={alpha(text, 0.65)} />} text={text} />
|
|
114
|
+
) : awaiting ? (
|
|
115
|
+
// Waiting is the intended state of a request, so it gets the
|
|
116
|
+
// theme's own colour, not warning amber.
|
|
117
|
+
<NeutralSeal icon={<Hourglass size={32} color={primary} />} text={text} />
|
|
118
|
+
) : requestEnded === "declined" ? (
|
|
119
|
+
<NeutralSeal icon={<Ban size={32} color={alpha(text, 0.65)} />} text={text} />
|
|
120
|
+
) : requestEnded === "expired" ? (
|
|
121
|
+
<NeutralSeal icon={<TimerOff size={32} color={alpha(text, 0.65)} />} text={text} />
|
|
94
122
|
) : (
|
|
95
123
|
<WarnSeal icon={<AlertTriangle size={34} color="#f59e0b" />} />
|
|
96
124
|
)}
|
|
@@ -103,7 +131,12 @@ export function EventConfirmation({
|
|
|
103
131
|
fontWeight: 700,
|
|
104
132
|
letterSpacing: "0.22em",
|
|
105
133
|
textTransform: "uppercase",
|
|
106
|
-
color:
|
|
134
|
+
color:
|
|
135
|
+
paid || awaiting
|
|
136
|
+
? primary
|
|
137
|
+
: refunded || requestEnded === "declined" || requestEnded === "expired"
|
|
138
|
+
? alpha(text, 0.65)
|
|
139
|
+
: "#f59e0b",
|
|
107
140
|
animationDelay: "0.15s",
|
|
108
141
|
}}
|
|
109
142
|
>
|
|
@@ -111,22 +144,76 @@ export function EventConfirmation({
|
|
|
111
144
|
? t("forge.event_confirmation.eyebrow_paid")
|
|
112
145
|
: refunded
|
|
113
146
|
? TICKET_ORDER_REFUNDED_COPY.eyebrow
|
|
114
|
-
:
|
|
147
|
+
: awaiting
|
|
148
|
+
? t("forge.event_confirmation.eyebrow_awaiting")
|
|
149
|
+
: requestEnded === "declined"
|
|
150
|
+
? t("forge.event_confirmation.eyebrow_declined")
|
|
151
|
+
: requestEnded === "expired"
|
|
152
|
+
? t("forge.event_confirmation.eyebrow_expired")
|
|
153
|
+
: requestEnded === "payment_failed"
|
|
154
|
+
? t("forge.event_confirmation.eyebrow_request_payment_failed")
|
|
155
|
+
: t("forge.event_confirmation.eyebrow_status", { status: order.status.replace(/_/g, " ") })}
|
|
115
156
|
</p>
|
|
116
157
|
<h1 className="cf-rise" style={{ fontSize: 29, fontWeight: 800, lineHeight: 1.12, margin: "0 0 8px", animationDelay: "0.22s" }}>
|
|
117
158
|
{paid
|
|
118
159
|
? t("forge.event_confirmation.title_paid")
|
|
119
160
|
: refunded
|
|
120
161
|
? TICKET_ORDER_REFUNDED_COPY.heading
|
|
121
|
-
:
|
|
162
|
+
: awaiting
|
|
163
|
+
? t("forge.event_confirmation.title_awaiting")
|
|
164
|
+
: requestEnded === "declined"
|
|
165
|
+
? t("forge.event_confirmation.title_declined")
|
|
166
|
+
: requestEnded === "expired"
|
|
167
|
+
? t("forge.event_confirmation.title_expired")
|
|
168
|
+
: requestEnded === "payment_failed"
|
|
169
|
+
? t("forge.event_confirmation.title_request_payment_failed")
|
|
170
|
+
: t("forge.event_confirmation.title_incomplete")}
|
|
122
171
|
</h1>
|
|
123
172
|
<p className="cf-rise" style={{ fontSize: 15, color: alpha(text, 0.7), margin: 0, animationDelay: "0.3s" }}>
|
|
124
173
|
{paid
|
|
125
174
|
? t("forge.event_confirmation.body_paid", { email: order.email })
|
|
126
175
|
: refunded
|
|
127
176
|
? TICKET_ORDER_REFUNDED_COPY.body
|
|
128
|
-
:
|
|
177
|
+
: awaiting
|
|
178
|
+
? t("forge.event_confirmation.body_awaiting_no_date")
|
|
179
|
+
: requestEnded === "declined"
|
|
180
|
+
? t("forge.event_confirmation.body_declined")
|
|
181
|
+
: requestEnded === "expired"
|
|
182
|
+
? t("forge.event_confirmation.body_expired")
|
|
183
|
+
: requestEnded === "payment_failed"
|
|
184
|
+
? t("forge.event_confirmation.body_request_payment_failed")
|
|
185
|
+
: t("forge.event_confirmation.body_incomplete", { status: order.status.replace(/_/g, " ") })}
|
|
129
186
|
</p>
|
|
187
|
+
{/* "Nothing is charged until they approve" is the promise the whole
|
|
188
|
+
request rests on. Content, not help: a fan who has just committed
|
|
189
|
+
a card reads it every time. */}
|
|
190
|
+
{awaiting && (
|
|
191
|
+
<p
|
|
192
|
+
className="cf-rise"
|
|
193
|
+
data-testid="request-nothing-charged"
|
|
194
|
+
style={{ fontSize: 14, fontWeight: 600, color: primary, margin: "10px 0 0", animationDelay: "0.34s" }}
|
|
195
|
+
>
|
|
196
|
+
{t("forge.event_confirmation.awaiting_nothing_charged")}
|
|
197
|
+
</p>
|
|
198
|
+
)}
|
|
199
|
+
{/* The organiser's own words, when they gave any. Theirs, so quoted
|
|
200
|
+
rather than paraphrased. */}
|
|
201
|
+
{requestEnded === "declined" && order.approvalDecisionNote && (
|
|
202
|
+
<blockquote
|
|
203
|
+
data-testid="request-decision-note"
|
|
204
|
+
style={{
|
|
205
|
+
margin: "14px auto 0",
|
|
206
|
+
padding: "10px 14px",
|
|
207
|
+
maxWidth: 400,
|
|
208
|
+
fontSize: 14,
|
|
209
|
+
color: alpha(text, 0.8),
|
|
210
|
+
borderLeft: `3px solid ${alpha(text, 0.18)}`,
|
|
211
|
+
textAlign: "left",
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
{order.approvalDecisionNote}
|
|
215
|
+
</blockquote>
|
|
216
|
+
)}
|
|
130
217
|
</div>
|
|
131
218
|
|
|
132
219
|
{/* ── Event summary ── */}
|
|
@@ -209,7 +296,9 @@ export function EventConfirmation({
|
|
|
209
296
|
? t("forge.event_confirmation.total_paid")
|
|
210
297
|
: refunded
|
|
211
298
|
? TICKET_ORDER_REFUNDED_COPY.totalLabel
|
|
212
|
-
:
|
|
299
|
+
: awaiting
|
|
300
|
+
? t("forge.event_confirmation.total_on_approval")
|
|
301
|
+
: t("forge.event_confirmation.total")}
|
|
213
302
|
</span>
|
|
214
303
|
{/* The exact charged total (charged currency) when present — never a re-converted catalog amount. */}
|
|
215
304
|
<span style={{ fontSize: 18, fontWeight: 800, color: primary }}>
|
|
@@ -242,7 +331,10 @@ export function EventConfirmation({
|
|
|
242
331
|
{/* ── Action ── */}
|
|
243
332
|
<div className="cf-rise" style={{ padding: "20px 30px 30px", animationDelay: "0.52s" }}>
|
|
244
333
|
<a
|
|
245
|
-
|
|
334
|
+
// A request that lapsed or could not be settled sends the fan back
|
|
335
|
+
// to the event, which may still have seats; everything else browses.
|
|
336
|
+
href={requestEnded === "expired" || requestEnded === "payment_failed" ? tryAgainPath : explorePath}
|
|
337
|
+
data-testid="confirmation-action"
|
|
246
338
|
style={{
|
|
247
339
|
display: "inline-flex",
|
|
248
340
|
alignItems: "center",
|
|
@@ -257,7 +349,12 @@ export function EventConfirmation({
|
|
|
257
349
|
textDecoration: "none",
|
|
258
350
|
}}
|
|
259
351
|
>
|
|
260
|
-
{
|
|
352
|
+
{requestEnded === "payment_failed"
|
|
353
|
+
? t("forge.event_confirmation.buy_again")
|
|
354
|
+
: requestEnded === "expired"
|
|
355
|
+
? t("forge.event_confirmation.view_event")
|
|
356
|
+
: t("forge.event_confirmation.explore")}{" "}
|
|
357
|
+
<ArrowRight size={16} />
|
|
261
358
|
</a>
|
|
262
359
|
</div>
|
|
263
360
|
</ConfirmationCard>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type CSSProperties, type ReactNode, useEffect, useMemo, useState } from "react";
|
|
2
|
-
import { AlertCircle, CalendarClock, Clock, KeyRound, X } from "lucide-react";
|
|
2
|
+
import { AlertCircle, CalendarClock, Clock, KeyRound, ShieldCheck, X } from "lucide-react";
|
|
3
3
|
import type { IEvent, ITicket } from "../../types/models";
|
|
4
4
|
import { useEventCheckout } from "../headless/event/useEventCheckout";
|
|
5
5
|
import { useForgeT } from "../../i18n";
|
|
@@ -26,6 +26,8 @@ import { MembershipGateNotice } from "./MembershipGateNotice";
|
|
|
26
26
|
import { DiscountCodeField, DiscountSummaryLines } from "./DiscountCode";
|
|
27
27
|
import { PresaleCodeField } from "./PresaleCode";
|
|
28
28
|
import { HoldNotice } from "./HoldNotice";
|
|
29
|
+
import { CallHelpHint } from "../media/CallHelpHint";
|
|
30
|
+
import { isDeferredSettlement, deferredChargeNotice } from "../payment/stripeConfirmOutcome";
|
|
29
31
|
import {
|
|
30
32
|
DialogRoot,
|
|
31
33
|
DialogTrigger,
|
|
@@ -401,6 +403,16 @@ function TicketStep({
|
|
|
401
403
|
const gate =
|
|
402
404
|
event.membershipGate && !event.membershipGate.allowed ? event.membershipGate : ticket.membershipGate;
|
|
403
405
|
const isGated = !!gate && !gate.allowed;
|
|
406
|
+
/**
|
|
407
|
+
* Ticket requests (curated access, feature 3). The badge says the
|
|
408
|
+
* tier is requested, not bought; the `?` beside it says what that
|
|
409
|
+
* means for the fan's card. `isMixBlocked` is the one rule the
|
|
410
|
+
* picker enforces itself: one order is either a request or a
|
|
411
|
+
* purchase, so once the selection is one kind the other kind's `+`
|
|
412
|
+
* goes dark, and the sentence under the row says why.
|
|
413
|
+
*/
|
|
414
|
+
const needsApproval = ticket.requiresApproval === true;
|
|
415
|
+
const isMixBlocked = c.isTierBlockedByApprovalMix(ticket.id);
|
|
404
416
|
const borderColor = isExpired ? "#ef4444" : u && !u.expired ? a(u.color, 0.5) : a(theme.colors.primary, 0.25);
|
|
405
417
|
return (
|
|
406
418
|
<div
|
|
@@ -471,6 +483,31 @@ function TicketStep({
|
|
|
471
483
|
{onSaleBadge(saleWindow.startsAt, new Date(now))}
|
|
472
484
|
</div>
|
|
473
485
|
)}
|
|
486
|
+
{needsApproval && (
|
|
487
|
+
<div
|
|
488
|
+
data-testid="ticket-approval-badge"
|
|
489
|
+
style={{
|
|
490
|
+
display: "inline-flex",
|
|
491
|
+
alignItems: "center",
|
|
492
|
+
gap: 4,
|
|
493
|
+
padding: "2px 8px",
|
|
494
|
+
borderRadius: 6,
|
|
495
|
+
fontSize: 12,
|
|
496
|
+
fontWeight: 700,
|
|
497
|
+
marginBottom: 6,
|
|
498
|
+
marginRight: 6,
|
|
499
|
+
background: a(theme.colors.primary, 0.1),
|
|
500
|
+
color: theme.colors.primary,
|
|
501
|
+
border: `1px solid ${a(theme.colors.primary, 0.3)}`,
|
|
502
|
+
}}
|
|
503
|
+
>
|
|
504
|
+
<ShieldCheck size={12} />
|
|
505
|
+
{t("forge.event_tickets.approval_badge")}
|
|
506
|
+
<CallHelpHint label={t("forge.event_tickets.approval_help_label")}>
|
|
507
|
+
{t("forge.event_tickets.approval_help_body")}
|
|
508
|
+
</CallHelpHint>
|
|
509
|
+
</div>
|
|
510
|
+
)}
|
|
474
511
|
{u && !u.expired && (
|
|
475
512
|
<div
|
|
476
513
|
style={{
|
|
@@ -546,7 +583,9 @@ function TicketStep({
|
|
|
546
583
|
// but because letting a buyer build a cart the server will
|
|
547
584
|
// not sell is precisely the "discoverable only by failing"
|
|
548
585
|
// journey this change exists to end.
|
|
549
|
-
disabled={
|
|
586
|
+
disabled={
|
|
587
|
+
selectedQty >= maxAllowed || isSoldOut || isExpired || isGated || isScheduled || isMixBlocked
|
|
588
|
+
}
|
|
550
589
|
style={stepperStyle(theme, true)}
|
|
551
590
|
>
|
|
552
591
|
+
|
|
@@ -603,6 +642,18 @@ function TicketStep({
|
|
|
603
642
|
{seatNotice}
|
|
604
643
|
</div>
|
|
605
644
|
)}
|
|
645
|
+
{/* The `+` above went dark for this tier. That is state the fan
|
|
646
|
+
has to act on (remove the others, or leave this one), so it
|
|
647
|
+
stays visible; the reason lives behind the `?`. */}
|
|
648
|
+
{isMixBlocked && !isSoldOut && !isExpired && (
|
|
649
|
+
<div data-testid="ticket-approval-mix-notice" style={infoNoticeStyle(theme.colors.primary, theme)}>
|
|
650
|
+
<AlertCircle size={16} />
|
|
651
|
+
<span>{t("forge.event_tickets.approval_mix_blocked")}</span>
|
|
652
|
+
<CallHelpHint label={t("forge.event_tickets.approval_mix_help_label")}>
|
|
653
|
+
{t("forge.event_tickets.approval_mix_help_body")}
|
|
654
|
+
</CallHelpHint>
|
|
655
|
+
</div>
|
|
656
|
+
)}
|
|
606
657
|
{isExpired && (
|
|
607
658
|
<div
|
|
608
659
|
style={{
|
|
@@ -634,7 +685,12 @@ function TicketStep({
|
|
|
634
685
|
it has to be the happy path, with a direct tickets-only checkout as
|
|
635
686
|
the secondary exit. Without add-ons the original flow is untouched:
|
|
636
687
|
Continue goes straight to details, the cart is the secondary exit. */}
|
|
637
|
-
{
|
|
688
|
+
{/* A request cannot go to the cart: a bundle forces payment and the API
|
|
689
|
+
refuses approval tiers in one. So a request selection gets the plain
|
|
690
|
+
Continue with no cart exit, whether or not the page sells add-ons. */}
|
|
691
|
+
{c.awaitingApproval ? (
|
|
692
|
+
<ActionBar c={c} fmt={fmt} onNext={c.goToDetails} nextLabel={t("forge.event_tickets.continue")} />
|
|
693
|
+
) : hasAddons ? (
|
|
638
694
|
<ActionBar
|
|
639
695
|
c={c}
|
|
640
696
|
fmt={fmt}
|
|
@@ -851,12 +907,18 @@ function DetailsStep({
|
|
|
851
907
|
// fee and any discount: a free ticket with a FLAT booking fee still
|
|
852
908
|
// costs money and still goes to payment, and a 100%-off code on a paid
|
|
853
909
|
// ticket does not.
|
|
910
|
+
// A request says so on the button, priced or not: with a price the
|
|
911
|
+
// next screen authorises the card rather than charging it, and with
|
|
912
|
+
// none there is no next screen at all. Either way "Continue to
|
|
913
|
+
// payment" would promise a sale that is not happening.
|
|
854
914
|
nextLabel={
|
|
855
915
|
c.isProcessing
|
|
856
916
|
? t("forge.event_tickets.processing")
|
|
857
|
-
: c.
|
|
858
|
-
? t("forge.event_tickets.
|
|
859
|
-
:
|
|
917
|
+
: c.awaitingApproval
|
|
918
|
+
? t("forge.event_tickets.request_tickets")
|
|
919
|
+
: c.totalAmount > 0
|
|
920
|
+
? t("forge.event_tickets.continue_payment")
|
|
921
|
+
: t("forge.event_tickets.confirm")
|
|
860
922
|
}
|
|
861
923
|
nextDisabled={c.isProcessing}
|
|
862
924
|
/>
|
|
@@ -880,10 +942,17 @@ function PaymentStep({
|
|
|
880
942
|
// row (already contained in the charged total below); inclusive stores get a
|
|
881
943
|
// muted "Includes X tax" caption. Display only.
|
|
882
944
|
const taxSummary = summarizeTaxQuote(c.taxQuote);
|
|
945
|
+
// A priced ticket request: start-payment said the charge is deferred, so
|
|
946
|
+
// this screen authorises the card and the wording must not say "purchase".
|
|
947
|
+
const deferred = isDeferredSettlement(c.settlement);
|
|
883
948
|
return (
|
|
884
949
|
<div>
|
|
885
|
-
<h2 style={{ fontSize: 22, fontWeight: 700, marginBottom: 8 }}>
|
|
886
|
-
|
|
950
|
+
<h2 style={{ fontSize: 22, fontWeight: 700, marginBottom: 8 }}>
|
|
951
|
+
{deferred ? t("forge.event_tickets.authorize_title") : t("forge.event_tickets.payment_title")}
|
|
952
|
+
</h2>
|
|
953
|
+
<p style={{ fontSize: 14, opacity: 0.7, marginBottom: 16 }}>
|
|
954
|
+
{deferred ? t("forge.event_tickets.authorize_subtitle") : t("forge.event_tickets.payment_subtitle")}
|
|
955
|
+
</p>
|
|
887
956
|
{/* The reservation clock. Event ticket holds are taken unconditionally
|
|
888
957
|
(no feature switch), so this is the one checkout where the countdown
|
|
889
958
|
fires for every profile, and it was the one that did not render it. */}
|
|
@@ -954,17 +1023,42 @@ function PaymentStep({
|
|
|
954
1023
|
so it is checked BEFORE the "not configured" branch. */}
|
|
955
1024
|
{c.isPaystack ? (
|
|
956
1025
|
c.canOpenPaystack ? (
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1026
|
+
<>
|
|
1027
|
+
<PaystackPayButton
|
|
1028
|
+
onPay={c.openPaystackCheckout}
|
|
1029
|
+
label={
|
|
1030
|
+
deferred
|
|
1031
|
+
? t("forge.event_tickets.authorize", { amount: fmt(c.totalAmount) })
|
|
1032
|
+
: t("forge.event_tickets.pay", { amount: fmt(c.totalAmount) })
|
|
1033
|
+
}
|
|
1034
|
+
/>
|
|
1035
|
+
{/* Content, not help: the fan has just been told "Authorize" and
|
|
1036
|
+
must be told when the money moves. The built-in Stripe renderer
|
|
1037
|
+
draws its own copy of this line under its own button. */}
|
|
1038
|
+
{deferred && (
|
|
1039
|
+
<p data-testid="deferred-charge-notice" style={{ fontSize: 13, opacity: 0.8, marginTop: 8 }}>
|
|
1040
|
+
{t("forge.event_tickets.deferred_charged_on_approval")}
|
|
1041
|
+
{deferredChargeNotice(c.deferredStrategy) === "authorization" && (
|
|
1042
|
+
<> {t("forge.event_tickets.deferred_hold_on_statement")}</>
|
|
1043
|
+
)}
|
|
1044
|
+
</p>
|
|
1045
|
+
)}
|
|
1046
|
+
</>
|
|
961
1047
|
) : (
|
|
962
1048
|
<p style={{ opacity: 0.7 }}>{t("forge.event_tickets.preparing_payment")}</p>
|
|
963
1049
|
)
|
|
964
1050
|
) : !renderPayment ? (
|
|
965
1051
|
<p style={{ color: "#ef4444", fontSize: 14 }}>{t("forge.event_tickets.payment_not_configured")}</p>
|
|
966
1052
|
) : c.clientSecret ? (
|
|
967
|
-
renderPayment({
|
|
1053
|
+
renderPayment({
|
|
1054
|
+
clientSecret: c.clientSecret,
|
|
1055
|
+
returnUrl: c.returnUrl,
|
|
1056
|
+
amount: c.totalAmount,
|
|
1057
|
+
mode: "redirect",
|
|
1058
|
+
// Additive: a renderer that ignores these renders the sale it always did.
|
|
1059
|
+
settlement: c.settlement,
|
|
1060
|
+
deferredStrategy: c.deferredStrategy,
|
|
1061
|
+
})
|
|
968
1062
|
) : (
|
|
969
1063
|
<p style={{ opacity: 0.7 }}>{t("forge.event_tickets.preparing_payment")}</p>
|
|
970
1064
|
)}
|
|
@@ -17,10 +17,12 @@ export interface LoginFormProps {
|
|
|
17
17
|
* form looks exactly as it did.
|
|
18
18
|
*/
|
|
19
19
|
membershipTierId?: string;
|
|
20
|
+
/** The membership invite token riding on the same redirect, if any. See `MembershipTierCallout`. */
|
|
21
|
+
membershipInviteToken?: string;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/** Themed 2FA login form (credentials → emailed code), built on `useLoginFlow`. */
|
|
23
|
-
export function LoginForm({ onSuccess, signupHref, forgotHref, membershipTierId }: LoginFormProps) {
|
|
25
|
+
export function LoginForm({ onSuccess, signupHref, forgotHref, membershipTierId, membershipInviteToken }: LoginFormProps) {
|
|
24
26
|
const t = useForgeT();
|
|
25
27
|
const theme = useForgeTheme();
|
|
26
28
|
const flow = useLoginFlow({ onSuccess });
|
|
@@ -70,7 +72,7 @@ export function LoginForm({ onSuccess, signupHref, forgotHref, membershipTierId
|
|
|
70
72
|
<div style={card}>
|
|
71
73
|
{/* Still on the way to the tier they picked, so it stays in view while
|
|
72
74
|
they fetch the code out of their inbox. */}
|
|
73
|
-
<MembershipTierCallout membershipTierId={membershipTierId} />
|
|
75
|
+
<MembershipTierCallout membershipTierId={membershipTierId} inviteToken={membershipInviteToken} />
|
|
74
76
|
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 8 }}>{t("forge.login_form.verify_title")}</h1>
|
|
75
77
|
<p style={{ fontSize: 14, opacity: 0.7, marginBottom: 24 }}>
|
|
76
78
|
{t("forge.login_form.code_sent_to")} <strong>{flow.email}</strong>
|
|
@@ -118,7 +120,7 @@ export function LoginForm({ onSuccess, signupHref, forgotHref, membershipTierId
|
|
|
118
120
|
<div style={card}>
|
|
119
121
|
{/* What the fan was doing when they hit this wall. Nothing is drawn when
|
|
120
122
|
they came here on their own. */}
|
|
121
|
-
<MembershipTierCallout membershipTierId={membershipTierId} />
|
|
123
|
+
<MembershipTierCallout membershipTierId={membershipTierId} inviteToken={membershipInviteToken} />
|
|
122
124
|
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 24 }}>{t("forge.login_form.title")}</h1>
|
|
123
125
|
{flow.error && <p style={{ color: "#ef4444", marginBottom: 16 }}>{flow.error}</p>}
|
|
124
126
|
<form
|