Skip to content

Commit 9f851e8

Browse files
committed
feat: handling expired status payments
1 parent 6685d92 commit 9f851e8

File tree

5 files changed

+238
-43
lines changed

5 files changed

+238
-43
lines changed

src/features/events/components/ColumnsUserEventList.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ export const columnsUserEventList: ColumnDef<UserEventResponse>[] = [
3333
header: "Status",
3434
cell: ({ row }) => {
3535
const status = row.original.status;
36-
const variant = status === "SUCCESS" ? "open" : status === "PENDING" ? "soon" : "closed";
36+
const variant =
37+
status === "SUCCESS" ? "open" : status === "PENDING" ? "soon" : status === "EXPIRED" ? "closed" : "closed";
3738
return (
3839
<div>
3940
<Badge variant={variant}>{status}</Badge>

src/features/events/components/EventCheckStatusModal.tsx

Lines changed: 157 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,178 @@
11
import { Button } from "@/components/ui/Button";
22
import { useDialog } from "@/contexts";
3-
import { Check, CreditCard, HelpCircle } from "lucide-react";
4-
import { ReactNode } from "react";
3+
import { Check, CreditCard, HelpCircle, X, AlertCircle, RefreshCcw } from "lucide-react";
4+
import { ReactNode, useEffect } from "react";
5+
import { motion } from "motion/react";
6+
import { cn } from "@/lib/utils";
57

68
type Props = {
79
status?: string;
810
transaction_no?: string;
11+
onRefresh?: () => void;
912
};
1013

11-
const statusIconMap: Record<string, ReactNode> = {
12-
paid: <Check className="h-20 w-20 text-green-500" />,
13-
pending: <CreditCard className="h-20 w-20 text-yellow-500" />,
14+
type StatusConfig = {
15+
icon: ReactNode;
16+
title: string;
17+
description: string;
18+
bgColor: string;
19+
textColor: string;
20+
borderColor: string;
21+
action: "close" | "refresh" | "none";
22+
autoClose?: number;
1423
};
1524

16-
const EventCheckStatusModal = ({ status = "", transaction_no = "" }: Props) => {
25+
const statusConfigMap: Record<string, StatusConfig> = {
26+
SUCCESS: {
27+
icon: <Check className="h-16 w-16" />,
28+
title: "Payment Successful!",
29+
description: "Your payment has been confirmed. You're all set for the event!",
30+
bgColor: "bg-green-50 dark:bg-green-950/20",
31+
textColor: "text-green-600 dark:text-green-400",
32+
borderColor: "border-green-200 dark:border-green-800",
33+
action: "close",
34+
autoClose: 3000,
35+
},
36+
PENDING: {
37+
icon: <CreditCard className="h-16 w-16" />,
38+
title: "Payment Pending",
39+
description: "Your payment is still being processed. Please wait a moment and check again.",
40+
bgColor: "bg-yellow-50 dark:bg-yellow-950/20",
41+
textColor: "text-yellow-600 dark:text-yellow-500",
42+
borderColor: "border-yellow-200 dark:border-yellow-800",
43+
action: "refresh",
44+
},
45+
FAILED: {
46+
icon: <X className="h-16 w-16" />,
47+
title: "Payment Failed",
48+
description: "Your payment could not be processed. Please try again or contact support.",
49+
bgColor: "bg-red-50 dark:bg-red-950/20",
50+
textColor: "text-red-600 dark:text-red-400",
51+
borderColor: "border-red-200 dark:border-red-800",
52+
action: "close",
53+
},
54+
EXPIRED: {
55+
icon: <AlertCircle className="h-16 w-16" />,
56+
title: "Payment Expired",
57+
description: "Your payment link has expired. Please register again to get a new payment link.",
58+
bgColor: "bg-orange-50 dark:bg-orange-950/20",
59+
textColor: "text-orange-600 dark:text-orange-400",
60+
borderColor: "border-orange-200 dark:border-orange-800",
61+
action: "close",
62+
},
63+
};
64+
65+
const EventCheckStatusModal = ({ status = "", transaction_no = "", onRefresh }: Props) => {
1766
const { closeDialog } = useDialog();
1867

19-
const icon = statusIconMap[status] ?? <HelpCircle className="h-20 w-20 text-gray-400" />;
68+
const config =
69+
statusConfigMap[status.toUpperCase()] ||
70+
({
71+
icon: <HelpCircle className="h-16 w-16" />,
72+
title: "Unknown Status",
73+
description: "We couldn't determine the payment status. Please try again later.",
74+
bgColor: "bg-gray-50 dark:bg-gray-950/20",
75+
textColor: "text-gray-600 dark:text-gray-400",
76+
borderColor: "border-gray-200 dark:border-gray-800",
77+
action: "close",
78+
} as StatusConfig);
79+
80+
useEffect(() => {
81+
if (config.autoClose && status.toUpperCase() === "SUCCESS") {
82+
const timer = setTimeout(() => {
83+
closeDialog();
84+
onRefresh?.();
85+
}, config.autoClose);
86+
87+
return () => clearTimeout(timer);
88+
}
89+
}, [config.autoClose, status, closeDialog, onRefresh]);
90+
91+
const handleAction = () => {
92+
if (config.action === "refresh" && onRefresh) {
93+
closeDialog();
94+
setTimeout(() => {
95+
onRefresh();
96+
}, 300);
97+
} else {
98+
closeDialog();
99+
if (status.toUpperCase() === "SUCCESS") {
100+
onRefresh?.();
101+
}
102+
}
103+
};
20104

21105
return (
22-
<div className="flex flex-col items-center justify-center gap-6 py-8 text-center">
23-
{icon}
106+
<div className="flex flex-col items-center justify-center gap-6 py-6 text-center">
107+
<motion.div
108+
initial={{ scale: 0 }}
109+
animate={{ scale: 1 }}
110+
transition={{ type: "spring", duration: 0.5, bounce: 0.4 }}
111+
className={cn(
112+
"flex items-center justify-center rounded-full p-6",
113+
config.bgColor,
114+
config.borderColor,
115+
"border-2"
116+
)}
117+
>
118+
<motion.div
119+
initial={{ rotate: 0 }}
120+
animate={{ rotate: [0, 10, -10, 0] }}
121+
transition={{ duration: 0.5, delay: 0.2 }}
122+
className={config.textColor}
123+
>
124+
{config.icon}
125+
</motion.div>
126+
</motion.div>
127+
128+
<motion.div
129+
initial={{ opacity: 0, y: 20 }}
130+
animate={{ opacity: 1, y: 0 }}
131+
transition={{ duration: 0.3, delay: 0.2 }}
132+
className="space-y-2"
133+
>
134+
<h3 className="text-xl font-bold">{config.title}</h3>
135+
<p className="text-muted-foreground mx-auto max-w-sm text-sm">{config.description}</p>
24136

25-
<div className="space-y-1">
26-
<p className="text-xl font-semibold capitalize">{status || "Unknown"}</p>
27137
{transaction_no && (
28-
<p className="text-muted-foreground text-sm">
29-
Transaction No: <span className="font-medium">{transaction_no}</span>
30-
</p>
138+
<div className={cn("mt-4 rounded-lg border p-3", config.borderColor, config.bgColor)}>
139+
<p className="text-xs text-gray-500 dark:text-gray-400">Transaction Number</p>
140+
<p className={cn("font-mono text-sm font-semibold", config.textColor)}>{transaction_no}</p>
141+
</div>
142+
)}
143+
</motion.div>
144+
145+
<motion.div
146+
initial={{ opacity: 0 }}
147+
animate={{ opacity: 1 }}
148+
transition={{ duration: 0.3, delay: 0.4 }}
149+
className="flex w-full gap-3"
150+
>
151+
{config.action === "refresh" && (
152+
<Button variant="outline" className="flex-1" onClick={handleAction}>
153+
<RefreshCcw className="mr-2 h-4 w-4" />
154+
Check Again
155+
</Button>
31156
)}
32-
</div>
157+
<Button
158+
variant={status.toUpperCase() === "SUCCESS" ? "default" : "outline"}
159+
className={cn("flex-1", status.toUpperCase() === "SUCCESS" && "bg-green-600 hover:bg-green-700")}
160+
onClick={handleAction}
161+
>
162+
{status.toUpperCase() === "SUCCESS" ? "Great!" : "Close"}
163+
</Button>
164+
</motion.div>
33165

34-
<Button variant="outline" onClick={closeDialog}>
35-
Kembali
36-
</Button>
166+
{status.toUpperCase() === "SUCCESS" && config.autoClose && (
167+
<motion.p
168+
initial={{ opacity: 0 }}
169+
animate={{ opacity: 1 }}
170+
transition={{ duration: 0.3, delay: 0.5 }}
171+
className="text-muted-foreground text-xs"
172+
>
173+
This dialog will close automatically in 3 seconds...
174+
</motion.p>
175+
)}
37176
</div>
38177
);
39178
};

src/features/events/components/EventDetailModal.tsx

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
StepperDescription,
1212
} from "@/components/ui/Stepper";
1313
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/Tabs";
14-
import { Check, X, UserCheck, CreditCard, CheckCircle2, Calendar, User } from "lucide-react";
14+
import { Check, X, UserCheck, CreditCard, CheckCircle2, Calendar, User, AlertCircle } from "lucide-react";
1515
import { cn } from "@/lib/utils";
1616
import { Button } from "@/components/ui/Button";
1717
import { useRegistEvent } from "../hooks/useRegistEvent";
@@ -22,11 +22,12 @@ interface EventDetailModalProps {
2222

2323
export const EventDetailModal = ({ event }: EventDetailModalProps) => {
2424
const { event_detail, user_detail } = event;
25-
const { checkPaymentStatus } = useRegistEvent();
25+
const { checkPaymentStatus, isCheckingPayment } = useRegistEvent();
2626

2727
const getActiveStep = () => {
2828
if (event.status === "SUCCESS") return 3;
2929
if (event.status === "FAILED") return 3;
30+
if (event.status === "EXPIRED") return 3;
3031
if (event.status === "PENDING") return 2;
3132
return 1;
3233
};
@@ -116,14 +117,18 @@ export const EventDetailModal = ({ event }: EventDetailModalProps) => {
116117
event.status === "SUCCESS"
117118
? "text-white data-[state=completed]:bg-green-500"
118119
: event.status === "FAILED"
119-
? "bg-red-500 text-white"
120-
: "bg-black text-white"
120+
? "!bg-red-500 text-white"
121+
: event.status === "EXPIRED"
122+
? "!bg-red-500 text-white"
123+
: "bg-black text-white"
121124
}`}
122125
>
123126
{event.status === "SUCCESS" ? (
124127
<Check className="h-5 w-5" />
125128
) : event.status === "FAILED" ? (
126129
<X className="h-5 w-5" />
130+
) : event.status === "EXPIRED" ? (
131+
<AlertCircle className="h-5 w-5" />
127132
) : (
128133
<CheckCircle2 className="h-5 w-5" />
129134
)}
@@ -135,7 +140,9 @@ export const EventDetailModal = ({ event }: EventDetailModalProps) => {
135140
? "Payment Success"
136141
: event.status === "FAILED"
137142
? "Payment Failed"
138-
: "On Pending"}
143+
: event.status === "EXPIRED"
144+
? "Payment Expired"
145+
: "On Pending"}
139146
</span>
140147
</div>
141148
</StepperTrigger>
@@ -146,10 +153,18 @@ export const EventDetailModal = ({ event }: EventDetailModalProps) => {
146153
? "bg-green-500/10 text-green-500"
147154
: event.status === "FAILED"
148155
? "bg-red-500/10 text-red-500"
149-
: "bg-gray-500/10 text-gray-500 dark:text-gray-400"
156+
: event.status === "EXPIRED"
157+
? "bg-red-500/10 text-red-500"
158+
: "bg-gray-500/10 text-gray-500 dark:text-gray-400"
150159
}`}
151160
>
152-
{event.status === "SUCCESS" ? "Completed" : event.status === "FAILED" ? "Failed" : "Pending"}
161+
{event.status === "SUCCESS"
162+
? "Completed"
163+
: event.status === "FAILED"
164+
? "Failed"
165+
: event.status === "EXPIRED"
166+
? "Expired"
167+
: "Pending"}
153168
</span>
154169
</StepperDescription>
155170
</div>
@@ -159,21 +174,32 @@ export const EventDetailModal = ({ event }: EventDetailModalProps) => {
159174

160175
{event.status === "PENDING" && event.payment_url && (
161176
<div className="mt-4 flex flex-col items-center justify-center gap-4 rounded-md border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20">
162-
<a
163-
href={event.payment_url}
164-
target="_blank"
165-
rel="noopener noreferrer"
166-
className="text-sm font-medium text-blue-600 hover:text-blue-700 hover:underline dark:text-blue-400 dark:hover:text-blue-300"
167-
>
168-
Click here to complete your payment →
169-
</a>
177+
<div className="flex flex-col items-center gap-2">
178+
<p className="text-sm font-medium text-blue-900 dark:text-blue-100">Complete Your Payment</p>
179+
<a
180+
href={event.payment_url}
181+
target="_blank"
182+
rel="noopener noreferrer"
183+
className="text-sm font-medium text-blue-600 hover:text-blue-700 hover:underline dark:text-blue-400 dark:hover:text-blue-300"
184+
>
185+
Click here to open payment page →
186+
</a>
187+
</div>
170188
<Button
171-
className="cursor-pointer"
189+
className="w-full cursor-pointer"
172190
onClick={() => {
173191
checkPaymentStatus({ transaction_no: event.transaction_no });
174192
}}
193+
disabled={isCheckingPayment}
175194
>
176-
Check Payment
195+
{isCheckingPayment ? (
196+
<>
197+
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
198+
Checking...
199+
</>
200+
) : (
201+
"Check Payment Status"
202+
)}
177203
</Button>
178204
</div>
179205
)}

src/features/events/hooks/useRegistEvent.tsx

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import { toast } from "sonner";
44
import { uploadsService } from "@/services/uploads";
55
import { eventsService } from "@/services/events";
66
import { EventType, RegistrationForm } from "@/domains/Events";
7-
import { useMutation } from "@tanstack/react-query";
7+
import { useMutation, useQueryClient } from "@tanstack/react-query";
88
import { useDialog } from "@/contexts";
99
import EventCheckStatusModal from "../components/EventCheckStatusModal";
1010

1111
export const useRegistEvent = (data?: EventType) => {
1212
const t = useTranslations("EventsPage");
13-
const { openDialog } = useDialog();
13+
const { openDialog, closeDialog } = useDialog();
14+
const queryClient = useQueryClient();
1415
const [isLoading, setIsLoading] = useState<boolean>(false);
1516
const [nameImage, setNameImage] = useState<string | null>("");
1617
// const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
@@ -67,17 +68,45 @@ export const useRegistEvent = (data?: EventType) => {
6768
}
6869
};
6970

70-
const { mutate: checkPaymentStatus } = useMutation({
71+
const { mutate: checkPaymentStatus, isPending: isCheckingPayment } = useMutation({
7172
mutationKey: ["checkPaymentStatus"],
7273
mutationFn: async ({ transaction_no }: { transaction_no: string }) =>
7374
eventsService.checkPaymentStatus(transaction_no),
74-
onSuccess: (data) => {
75+
onSuccess: (data, variables) => {
76+
const paymentStatus = data?.data?.status;
77+
7578
openDialog({
76-
content: <EventCheckStatusModal status={data?.data?.status} transaction_no={data?.data?.transaction_no} />,
79+
content: (
80+
<EventCheckStatusModal
81+
status={paymentStatus}
82+
transaction_no={data?.data?.transaction_no}
83+
onRefresh={() => {
84+
closeDialog();
85+
setTimeout(() => {
86+
checkPaymentStatus({ transaction_no: variables.transaction_no });
87+
}, 300);
88+
}}
89+
/>
90+
),
7791
size: "sm",
7892
});
93+
94+
queryClient.invalidateQueries({ queryKey: ["getListMyEvents"] });
95+
96+
if (paymentStatus?.toUpperCase() === "SUCCESS") {
97+
setTimeout(() => {
98+
toast.success("Payment verified!", {
99+
description: "Your event registration is now confirmed.",
100+
});
101+
}, 3500);
102+
}
103+
},
104+
onError: () => {
105+
toast.error("Failed to check payment status", {
106+
description: "Please try again later or contact support if the problem persists.",
107+
});
79108
},
80109
});
81110

82-
return { registEvent, isLoading, checkPaymentStatus };
111+
return { registEvent, isLoading, checkPaymentStatus, isCheckingPayment };
83112
};

0 commit comments

Comments
 (0)