@tribe-nest/forge 3.24.0 → 3.26.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 +2 -1
- package/src/client/activeFunnel.ts +35 -0
- package/src/client/createForgeClient.ts +9 -0
- package/src/contexts/CartContext.tsx +8 -1
- package/src/data/queries/useForms.ts +48 -3
- package/src/data/queries/usePageActions.ts +2 -2
- package/src/types/models.ts +7 -0
- package/src/ui/headless/checkout/useCheckout.ts +50 -12
- package/src/ui/headless/event/useEventCheckout.ts +3 -0
- package/src/ui/headless/funnel/Funnel.tsx +13 -0
- package/src/ui/styled/BundleConfirmation.tsx +77 -46
- package/src/ui/styled/Checkout.tsx +41 -2
- package/src/ui/styled/EventTickets.tsx +84 -22
- package/src/ui/styled/MusicLinkPage.tsx +290 -137
- package/src/ui/styled/PageActions.tsx +13 -9
- package/src/ui/styled/ProductGrid.tsx +17 -4
- package/src/ui/styled/_tests/musicLinkChrome.spec.ts +48 -0
- package/src/ui/styled/musicLinkStyles.tsx +475 -0
- package/src/ui/styled/musicServiceIcons.ts +26 -0
- package/src/utils/contrast.ts +60 -0
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
ticketSeatState,
|
|
17
17
|
} from "../format/ticketAvailability";
|
|
18
18
|
import { usePricesIncludeTax } from "../../data/queries/useWebsite";
|
|
19
|
+
import { usePageActions } from "../../data/queries/usePageActions";
|
|
19
20
|
import { Loading } from "./Loading";
|
|
20
21
|
import { PaystackPayButton } from "./PaystackPayButton";
|
|
21
22
|
import { usePaymentRenderer, type PaymentRenderProps } from "../payment/ForgePaymentProvider";
|
|
@@ -80,6 +81,20 @@ export function EventTickets({
|
|
|
80
81
|
const c = useEventCheckout(slug, { finalisePath, onComplete });
|
|
81
82
|
const renderPay = renderPayment ?? registered ?? undefined;
|
|
82
83
|
|
|
84
|
+
// Does this page sell add-ons for this event? Same query key as the page's
|
|
85
|
+
// own <PageActions> mount, so this is a cache read, not a second request.
|
|
86
|
+
// When add-ons exist, the PRIMARY exit from ticket selection is the cart
|
|
87
|
+
// (that's what unlocks the add-on grid); direct payment stays the happy
|
|
88
|
+
// path only on pages without add-ons.
|
|
89
|
+
const { data: pageActionsData } = usePageActions("event_detail", c.event?.id);
|
|
90
|
+
const hasAddons = useMemo(
|
|
91
|
+
() =>
|
|
92
|
+
(pageActionsData?.actions ?? []).some(
|
|
93
|
+
(action) => action.type === "product_cards" && Boolean((action.config as Record<string, unknown>)?.isAddon),
|
|
94
|
+
),
|
|
95
|
+
[pageActionsData],
|
|
96
|
+
);
|
|
97
|
+
|
|
83
98
|
const [open, setOpen] = useState(false);
|
|
84
99
|
|
|
85
100
|
const tryClose = (next: boolean) => {
|
|
@@ -133,9 +148,20 @@ export function EventTickets({
|
|
|
133
148
|
renderPayment={renderPay}
|
|
134
149
|
loginPath={loginPath}
|
|
135
150
|
membershipPath={membershipPath}
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
|
|
151
|
+
hasAddons={hasAddons}
|
|
152
|
+
// Selection is safe in the cart now: close, and bring the
|
|
153
|
+
// freshly unlocked add-on grid into view so the buyer lands on
|
|
154
|
+
// the next step instead of wherever they left the page.
|
|
155
|
+
onAddedToCart={() => {
|
|
156
|
+
setOpen(false);
|
|
157
|
+
if (typeof document !== "undefined") {
|
|
158
|
+
setTimeout(() => {
|
|
159
|
+
document
|
|
160
|
+
.querySelector('[data-testid="addons-block"]')
|
|
161
|
+
?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
162
|
+
}, 100);
|
|
163
|
+
}
|
|
164
|
+
}}
|
|
139
165
|
/>
|
|
140
166
|
)}
|
|
141
167
|
</DialogContent>
|
|
@@ -152,6 +178,7 @@ function EventTicketsBody({
|
|
|
152
178
|
fmt,
|
|
153
179
|
renderPayment,
|
|
154
180
|
onAddedToCart,
|
|
181
|
+
hasAddons,
|
|
155
182
|
loginPath,
|
|
156
183
|
membershipPath,
|
|
157
184
|
}: {
|
|
@@ -160,6 +187,7 @@ function EventTicketsBody({
|
|
|
160
187
|
fmt: (n: number) => string;
|
|
161
188
|
renderPayment?: (props: PaymentRenderProps) => ReactNode;
|
|
162
189
|
onAddedToCart?: () => void;
|
|
190
|
+
hasAddons?: boolean;
|
|
163
191
|
loginPath?: string;
|
|
164
192
|
membershipPath?: string;
|
|
165
193
|
}) {
|
|
@@ -173,6 +201,7 @@ function EventTicketsBody({
|
|
|
173
201
|
event={event}
|
|
174
202
|
fmt={fmt}
|
|
175
203
|
onAddedToCart={onAddedToCart}
|
|
204
|
+
hasAddons={hasAddons}
|
|
176
205
|
loginPath={loginPath}
|
|
177
206
|
membershipPath={membershipPath}
|
|
178
207
|
/>
|
|
@@ -232,6 +261,7 @@ function TicketStep({
|
|
|
232
261
|
event,
|
|
233
262
|
fmt,
|
|
234
263
|
onAddedToCart,
|
|
264
|
+
hasAddons,
|
|
235
265
|
loginPath,
|
|
236
266
|
membershipPath,
|
|
237
267
|
}: {
|
|
@@ -239,6 +269,7 @@ function TicketStep({
|
|
|
239
269
|
event: IEvent;
|
|
240
270
|
fmt: (n: number) => string;
|
|
241
271
|
onAddedToCart?: () => void;
|
|
272
|
+
hasAddons?: boolean;
|
|
242
273
|
loginPath?: string;
|
|
243
274
|
membershipPath?: string;
|
|
244
275
|
}) {
|
|
@@ -533,21 +564,37 @@ function TicketStep({
|
|
|
533
564
|
</div>
|
|
534
565
|
|
|
535
566
|
{c.error && <p style={{ color: "#ef4444", fontSize: 14, marginTop: 12 }}>{c.error}</p>}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
onClick: () => {
|
|
567
|
+
{/* Two shapes of this bar. With add-ons on the page, the PRIMARY action
|
|
568
|
+
is the cart: that is the only path that unlocks the add-on grid, so
|
|
569
|
+
it has to be the happy path, with a direct tickets-only checkout as
|
|
570
|
+
the secondary exit. Without add-ons the original flow is untouched:
|
|
571
|
+
Continue goes straight to details, the cart is the secondary exit. */}
|
|
572
|
+
{hasAddons ? (
|
|
573
|
+
<ActionBar
|
|
574
|
+
c={c}
|
|
575
|
+
fmt={fmt}
|
|
576
|
+
onNext={() => {
|
|
547
577
|
if (c.addTicketsToCart()) onAddedToCart?.();
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
|
|
578
|
+
}}
|
|
579
|
+
nextLabel={c.ticketsInCart ? "Update cart & choose add-ons" : "Continue to add-ons"}
|
|
580
|
+
nextTestId="tickets-add-to-cart"
|
|
581
|
+
secondary={{ label: "Checkout tickets only", onClick: c.goToDetails, testId: "tickets-checkout-only" }}
|
|
582
|
+
/>
|
|
583
|
+
) : (
|
|
584
|
+
<ActionBar
|
|
585
|
+
c={c}
|
|
586
|
+
fmt={fmt}
|
|
587
|
+
onNext={c.goToDetails}
|
|
588
|
+
nextLabel="Continue"
|
|
589
|
+
secondary={{
|
|
590
|
+
label: c.ticketsInCart ? "Update cart" : "Add to cart",
|
|
591
|
+
onClick: () => {
|
|
592
|
+
if (c.addTicketsToCart()) onAddedToCart?.();
|
|
593
|
+
},
|
|
594
|
+
testId: "tickets-add-to-cart",
|
|
595
|
+
}}
|
|
596
|
+
/>
|
|
597
|
+
)}
|
|
551
598
|
</div>
|
|
552
599
|
);
|
|
553
600
|
}
|
|
@@ -692,7 +739,13 @@ function DetailsStep({
|
|
|
692
739
|
fmt={fmt}
|
|
693
740
|
onBack={() => c.setStep("tickets")}
|
|
694
741
|
onNext={onContinue}
|
|
695
|
-
|
|
742
|
+
// Nothing to pay means no payment step follows, so promising one is a
|
|
743
|
+
// lie the next screen immediately contradicts. Read off `totalAmount`
|
|
744
|
+
// rather than the ticket prices because it already carries the booking
|
|
745
|
+
// fee and any discount: a free ticket with a FLAT booking fee still
|
|
746
|
+
// costs money and still goes to payment, and a 100%-off code on a paid
|
|
747
|
+
// ticket does not.
|
|
748
|
+
nextLabel={c.isProcessing ? "Processing…" : c.totalAmount > 0 ? "Continue to payment" : "Confirm"}
|
|
696
749
|
nextDisabled={c.isProcessing}
|
|
697
750
|
/>
|
|
698
751
|
</div>
|
|
@@ -865,6 +918,7 @@ function ActionBar({
|
|
|
865
918
|
onNext,
|
|
866
919
|
nextLabel,
|
|
867
920
|
nextDisabled,
|
|
921
|
+
nextTestId,
|
|
868
922
|
secondary,
|
|
869
923
|
}: {
|
|
870
924
|
c: Checkout;
|
|
@@ -873,8 +927,11 @@ function ActionBar({
|
|
|
873
927
|
onNext: () => void;
|
|
874
928
|
nextLabel: string;
|
|
875
929
|
nextDisabled?: boolean;
|
|
876
|
-
/**
|
|
877
|
-
|
|
930
|
+
/** Test id for the primary button; the caller says which button is which
|
|
931
|
+
action, because add-to-cart moves between slots depending on add-ons. */
|
|
932
|
+
nextTestId?: string;
|
|
933
|
+
/** Extra exit shown next to Back on the selection step. */
|
|
934
|
+
secondary?: { label: string; onClick: () => void; testId?: string };
|
|
878
935
|
}) {
|
|
879
936
|
const theme = useForgeTheme();
|
|
880
937
|
return (
|
|
@@ -904,7 +961,7 @@ function ActionBar({
|
|
|
904
961
|
</div>
|
|
905
962
|
<div style={{ display: "flex", gap: 8, justifyContent: "space-between" }}>
|
|
906
963
|
{secondary ? (
|
|
907
|
-
<button data-testid=
|
|
964
|
+
<button data-testid={secondary.testId} onClick={secondary.onClick} style={secondaryButtonStyle(theme)}>
|
|
908
965
|
{secondary.label}
|
|
909
966
|
</button>
|
|
910
967
|
) : (
|
|
@@ -912,7 +969,12 @@ function ActionBar({
|
|
|
912
969
|
Back
|
|
913
970
|
</button>
|
|
914
971
|
)}
|
|
915
|
-
<button
|
|
972
|
+
<button
|
|
973
|
+
data-testid={nextTestId}
|
|
974
|
+
onClick={onNext}
|
|
975
|
+
disabled={nextDisabled}
|
|
976
|
+
style={{ ...primaryButtonStyle(theme), opacity: nextDisabled ? 0.6 : 1 }}
|
|
977
|
+
>
|
|
916
978
|
{nextLabel}
|
|
917
979
|
</button>
|
|
918
980
|
</div>
|