import { useAuthSlice } from "@/app/providers";
import { eventQueries } from "@/entities/event";
import type { Order } from "@/entities/order";
import type { Socials } from "@/entities/user";
import { useSendTx } from "@/shared/hooks/sendTx";
import { formatUsdt } from "@/shared/lib/money/usdt";
import { pluralTickets } from "@/shared/lib/pluralDays";
import { getCurrencySymbol } from "@/shared/lib/web3";
import { Loader } from "@/shared/ui/Loader";
import { ResultDialog } from "@/shared/ui/ResultDialog";
import { Button } from "@/shared/ui/button";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router";
import { useActiveAccount } from "thirdweb/react";
import { purchase } from "../api/purchase";
export type ConfirmPaymentProps = {
order: Order;
referralAddress?: string;
onStageChange?: (stage: "checkout" | "pending" | "success" | "error") => void;
};
export const ConfirmPayment: React.FC<ConfirmPaymentProps> = ({
order,
referralAddress,
onStageChange,
}) => {
const navigate = useNavigate();
const account = useActiveAccount();
const { user, startTicketPoll } = useAuthSlice();
const { sendTx, data: txHash, error } = useSendTx();
const [hasStartedPayment, setHasStartedPayment] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [redirectEventId, setRedirectEventId] = useState<number | null>(null);
const [createEventTxHash, setCreateEventTxHash] = useState<string | null>(
null,
);
const queryClient = useQueryClient();
useEffect(() => {
setHasStartedPayment(false);
}, [order.type]);
const resolveCreatedEventId = useCallback(async () => {
if (!user || order.type !== "create_event") return null;
const events = await queryClient.fetchQuery(eventQueries.list());
if (!events) return null;
const targetPlaceId = Number(order.event.place);
const eventsArray = Array.isArray(events) ? events : [];
const matchedEvent = eventsArray.find(
(event: {
creator: { address: string };
title: string;
startDateTimestamp: number;
place: { id: number };
ticketPrice: string;
daysAmount: number;
id?: number;
}) => {
return (
event.creator.address === user.address &&
event.title === order.event.title &&
event.startDateTimestamp === order.event.startTimeTimeStamp &&
event.place.id === targetPlaceId &&
event.ticketPrice === order.event.ticketPrice &&
event.daysAmount === order.event.daysAmount
);
},
);
return matchedEvent?.id ?? null;
}, [order, queryClient, user]);
const { mutate, isPending } = useMutation({
mutationFn: (order: Order) => {
if (!account || !user) {
return Promise.reject("Account or user are missing");
}
return purchase(
sendTx,
account,
order,
user.socials[0] as Socials,
referralAddress,
);
},
onSuccess: async (returnedTxHash: string | undefined) => {
// Show success modal for all order types
setIsSuccess(true);
onStageChange?.("success");
if (order.type === "buy_ticket") {
// Ticket becomes visible only after the backend indexer syncs it to DB.
// Start polling current user until ticket(s) show up (or timeout).
const boughtCount = order.ticket.allocations.reduce(
(sum, a) => sum + a.count,
0,
);
const baseline = (user?.tickets ?? []).filter(
(t) => t.eventId === order.ticket.eventId,
).length;
startTicketPoll({
eventId: order.ticket.eventId,
targetCount: baseline + boughtCount,
});
setRedirectEventId(order.ticket.eventId);
return;
}
if (order.type === "update_event") {
setRedirectEventId(order.event.id);
return;
}
if (order.type === "create_event") {
// Store the txHash for navigation to waiting page
const txHashToUse = returnedTxHash || (txHash as string | null);
if (txHashToUse) {
setCreateEventTxHash(txHashToUse);
}
return;
}
},
onError: (e) => {
setHasStartedPayment(false);
onStageChange?.("error");
console.error(e);
},
});
if (!user) return null;
if (user.socials.length === 0)
return (
<div className="flex flex-col items-center gap-2 py-5">
<p className="text-lg">No socials are provided</p>
<Button onClick={() => navigate("/socials")}>Set socials</Button>
</div>
);
const handleConfirm = async () => {
// For create_event, navigate to the waiting page first
if (order.type === "create_event" && createEventTxHash) {
navigate(`/txhash/${createEventTxHash}`, { replace: true });
return;
}
let nextRedirectId = redirectEventId;
if (order.type === "create_event" && nextRedirectId == null) {
const createdId = await resolveCreatedEventId();
if (createdId != null) {
setRedirectEventId(createdId);
nextRedirectId = createdId;
}
}
if (nextRedirectId != null) {
queryClient.invalidateQueries({
queryKey: ["events", "lists", nextRedirectId],
});
queryClient.invalidateQueries({
queryKey: ["events", nextRedirectId, "attendees"],
});
navigate(`/events/${nextRedirectId}`, { replace: true });
return;
}
navigate("/", { replace: true });
};
const successMessage =
order.type === "buy_ticket"
? "Your will recieve your ticket within several minutes."
: order.type === "create_event"
? "Your event request has been submitted. Click proceed to track the confirmation."
: "Your order will be published within several minutes.";
// Calculate total price and ticket count for display
const { totalPrice, totalTickets } =
order.type === "buy_ticket"
? {
totalPrice: order.ticket.allocations.reduce((sum, a) => {
return sum + BigInt(a.count) * a.finalPricePerTicket;
}, 0n),
totalTickets: order.ticket.allocations.reduce(
(sum, a) => sum + a.count,
0,
),
}
: { totalPrice: null, totalTickets: null };
return (
<article className="card border-primary/30">
{error && <PaymentFailed cause={error} />}
{isPending || hasStartedPayment ? (
<div className="flex flex-col items-center gap-3 py-10">
<Loader />
<p className="text-sm text-muted-foreground text-center">
Processing transaction. Confirm in your wallet if prompted.
</p>
</div>
) : (
<div className="flex justify-center py-6">
<span>
<Button
onClick={() => {
setHasStartedPayment(true);
onStageChange?.("pending");
mutate(order);
}}
className="mx-auto"
>
{error
? "Try again"
: totalPrice !== null
? `Pay ${formatUsdt(totalPrice)} (${pluralTickets(totalTickets)})`
: "Confirm"}
{totalPrice !== null && (
<img
src={getCurrencySymbol()}
className="h-4 aspect-square inline ml-1"
alt="currency"
/>
)}
</Button>
</span>
</div>
)}
<ResultDialog
open={isSuccess}
setOpen={setIsSuccess}
title="Payment successful!"
body={successMessage}
onConfirm={handleConfirm}
txHash={txHash as string}
/>
</article>
);
};
type PaymentFailedProps = {
cause: Error;
};
function PaymentFailed({ cause }: PaymentFailedProps) {
let errorMessage = "Unexpected failure of transaction sending";
if (cause.message.includes("Not enough tokens")) {
errorMessage = "Not enough tokens";
}
return (
<div>
<img
className="aspect-square h-40 mx-auto my-7"
src="/icons/price_1.svg"
alt="purchase"
/>
<h2 className="text-muted font-semibold text-lg text-center">
Error during transaction
</h2>
<p className="text-muted/70 text-md text-center">{errorMessage}</p>
</div>
);
}