1- import { NavLink , Outlet , useNavigate , useParams } from "react-router" ;
1+ import { Outlet , useBlocker , useNavigate , useParams } from "react-router" ;
22import classes from './Checkout.module.scss' ;
33import { useGetOrderPublic } from "../../../queries/useGetOrderPublic.ts" ;
44import { t } from "@lingui/macro" ;
55import { Countdown } from "../../common/Countdown" ;
66import { CheckoutSidebar } from "./CheckoutSidebar" ;
77import { ActionIcon , Button , Group , Modal , Tooltip } from "@mantine/core" ;
88import { IconArrowLeft , IconPrinter , IconReceipt } from "@tabler/icons-react" ;
9- import { eventHomepageUrl } from "../../../utilites/urlHelper.ts" ;
9+ import { eventHomepagePath , eventHomepageUrl } from "../../../utilites/urlHelper.ts" ;
1010import { ShareComponent } from "../../common/ShareIcon" ;
1111import { AddToEventCalendarButton } from "../../common/AddEventToCalendarButton" ;
1212import { useMediaQuery } from "@mantine/hooks" ;
13- import { useState } from "react" ;
13+ import { useEffect , useState } from "react" ;
1414import { Invoice } from "../../../types.ts" ;
1515import { orderClientPublic } from "../../../api/order.client.ts" ;
1616import { downloadBinary } from "../../../utilites/download.ts" ;
1717import { withLoadingNotification } from "../../../utilites/withLoadingNotification.tsx" ;
18+ import { useAbandonOrderPublic } from "../../../mutations/useAbandonOrderPublic.ts" ;
19+ import { showError , showInfo } from "../../../utilites/notifications.tsx" ;
20+ import { isDateInFuture } from "../../../utilites/dates.ts" ;
1821
1922const Checkout = ( ) => {
2023 const { eventId, orderShortId} = useParams ( ) ;
@@ -27,6 +30,25 @@ const Checkout = () => {
2730 const isMobile = useMediaQuery ( '(max-width: 768px)' ) ;
2831 const [ isExpired , setIsExpired ] = useState ( false ) ;
2932 const orderHasAttendees = order ?. attendees && order . attendees . length > 0 ;
33+ const [ showAbandonDialog , setShowAbandonDialog ] = useState ( false ) ;
34+ const [ pendingNavigation , setPendingNavigation ] = useState < string | null > ( null ) ;
35+ const [ isAbandoning , setIsAbandoning ] = useState ( false ) ;
36+ const abandonOrderMutation = useAbandonOrderPublic ( ) ;
37+
38+ const isOrderReservedAndNotExpired = orderIsReserved && order ?. reserved_until
39+ && isDateInFuture ( order . reserved_until ) ;
40+
41+ const blocker = useBlocker (
42+ ( { currentLocation, nextLocation} ) => {
43+ const isLeavingCheckout = ! nextLocation . pathname . startsWith ( '/checkout/' ) ;
44+ return (
45+ ! isAbandoning &&
46+ ! ! isOrderReservedAndNotExpired &&
47+ currentLocation . pathname !== nextLocation . pathname &&
48+ isLeavingCheckout
49+ ) ;
50+ }
51+ ) ;
3052
3153 const handleExpiry = ( ) => {
3254 setIsExpired ( true ) ;
@@ -59,6 +81,54 @@ const Checkout = () => {
5981 ) ;
6082 }
6183
84+ const handleAbandonConfirm = async ( ) => {
85+ setIsAbandoning ( true ) ;
86+ try {
87+ await abandonOrderMutation . mutateAsync ( {
88+ eventId : Number ( eventId ) ,
89+ orderShortId : String ( orderShortId ) ,
90+ } ) ;
91+ } catch ( error ) {
92+ showError ( t `Failed to abandon order. Please try again.` ) ;
93+ } finally {
94+ setShowAbandonDialog ( false ) ;
95+ showInfo ( t `Your order has been cancelled.` ) ;
96+
97+ if ( blocker . state === 'blocked' ) {
98+ blocker . proceed ( ) ;
99+ } else if ( pendingNavigation ) {
100+ navigate ( pendingNavigation ) ;
101+ }
102+
103+ setPendingNavigation ( null ) ;
104+ setIsAbandoning ( false ) ;
105+ }
106+ } ;
107+
108+ const handleAbandonCancel = ( ) => {
109+ if ( blocker . state === 'blocked' ) {
110+ blocker . reset ( ) ;
111+ }
112+ setShowAbandonDialog ( false ) ;
113+ setPendingNavigation ( null ) ;
114+ } ;
115+
116+ const handleEventHomepageClick = ( e : React . MouseEvent ) => {
117+ if ( isOrderReservedAndNotExpired && event ) {
118+ e . preventDefault ( ) ;
119+ setPendingNavigation ( eventHomepagePath ( event ) ) ;
120+ setShowAbandonDialog ( true ) ;
121+ } else if ( event ) {
122+ navigate ( eventHomepagePath ( event ) ) ;
123+ }
124+ } ;
125+
126+ useEffect ( ( ) => {
127+ if ( blocker . state === 'blocked' ) {
128+ setShowAbandonDialog ( true ) ;
129+ }
130+ } , [ blocker . state ] ) ;
131+
62132 return (
63133 < >
64134 < div className = { classes . container } >
@@ -69,10 +139,9 @@ const Checkout = () => {
69139 < Group justify = "space-between" wrap = "nowrap" >
70140 < Button
71141 title = { t `Back to event page` }
72- component = { NavLink }
73142 variant = "subtle"
74143 leftSection = { < IconArrowLeft size = { 20 } /> }
75- to = { eventHomepageUrl ( event ) }
144+ onClick = { handleEventHomepageClick }
76145 >
77146 { ! isMobile && t `Event Homepage` }
78147 </ Button >
@@ -164,6 +233,39 @@ const Checkout = () => {
164233 </ Button >
165234 </ div >
166235 </ Modal >
236+
237+ < Modal
238+ opened = { showAbandonDialog }
239+ onClose = { handleAbandonCancel }
240+ withCloseButton = { false }
241+ centered
242+ size = "m"
243+ >
244+ < div style = { { textAlign : 'center' , padding : '20px 0' } } >
245+ < h3 >
246+ { t `Are you sure you want to leave?` }
247+ </ h3 >
248+ < p >
249+ { t `Your current order will be lost.` }
250+ </ p >
251+ < Group justify = "center" gap = "md" mt = "xl" >
252+ < Button
253+ onClick = { handleAbandonCancel }
254+ variant = "subtle"
255+ >
256+ { t `No, keep me here` }
257+ </ Button >
258+ < Button
259+ onClick = { handleAbandonConfirm }
260+ variant = "filled"
261+ color = "red"
262+ loading = { abandonOrderMutation . isPending }
263+ >
264+ { t `Yes, cancel my order` }
265+ </ Button >
266+ </ Group >
267+ </ div >
268+ </ Modal >
167269 </ >
168270 ) ;
169271}
0 commit comments