mirror of
https://github.com/andwati/cymelle.git
synced 2026-11-15 21:15:12 +00:00
purge paystack
This commit is contained in:
@@ -2,9 +2,8 @@ import { apiRequest } from "#/api/client";
|
||||
import type {
|
||||
CheckoutQuote,
|
||||
CheckoutQuoteRequest,
|
||||
PaystackInitializeRequest,
|
||||
PaystackInitializeResponse,
|
||||
PaystackVerifyResponse,
|
||||
SimulatedCheckoutRequest,
|
||||
SimulatedCheckoutResponse,
|
||||
} from "#/types/payment";
|
||||
|
||||
export function getCheckoutQuote(
|
||||
@@ -18,21 +17,9 @@ export function getCheckoutQuote(
|
||||
});
|
||||
}
|
||||
|
||||
export function initializePaystackPayment(request: PaystackInitializeRequest) {
|
||||
return apiRequest<PaystackInitializeResponse>(
|
||||
"/payments/paystack/initialize",
|
||||
{
|
||||
method: "POST",
|
||||
body: request,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function verifyPaystackPayment(reference: string) {
|
||||
return apiRequest<PaystackVerifyResponse>(
|
||||
`/payments/paystack/verify/${encodeURIComponent(reference)}`,
|
||||
{
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
export function simulateCheckout(request: SimulatedCheckoutRequest) {
|
||||
return apiRequest<SimulatedCheckoutResponse>("/checkout/simulate", {
|
||||
method: "POST",
|
||||
body: request,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
getCheckoutQuote,
|
||||
initializePaystackPayment,
|
||||
verifyPaystackPayment,
|
||||
} from "#/api/payments";
|
||||
import { getCheckoutQuote, simulateCheckout } from "#/api/payments";
|
||||
import { inventoryQueryKeys } from "#/hooks/useInventory";
|
||||
import { orderQueryKeys } from "#/hooks/useOrders";
|
||||
import { productQueryKeys } from "#/hooks/useProducts";
|
||||
import type {
|
||||
CheckoutQuoteRequest,
|
||||
PaystackInitializeRequest,
|
||||
SimulatedCheckoutRequest,
|
||||
} from "#/types/payment";
|
||||
|
||||
export const paymentQueryKeys = {
|
||||
@@ -29,27 +25,12 @@ export function useCheckoutQuote(
|
||||
});
|
||||
}
|
||||
|
||||
export function useInitializePaystackPayment() {
|
||||
export function useSimulateCheckout() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (request: PaystackInitializeRequest) =>
|
||||
initializePaystackPayment(request),
|
||||
onSuccess: async () => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: orderQueryKeys.all }),
|
||||
queryClient.invalidateQueries({ queryKey: inventoryQueryKeys.all }),
|
||||
queryClient.invalidateQueries({ queryKey: productQueryKeys.all }),
|
||||
]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useVerifyPaystackPayment() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: verifyPaystackPayment,
|
||||
mutationFn: (request: SimulatedCheckoutRequest) =>
|
||||
simulateCheckout(request),
|
||||
onSuccess: async () => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: orderQueryKeys.all }),
|
||||
|
||||
@@ -35,7 +35,6 @@ import { Route as DriverRidesIndexRouteImport } from './routes/driver.rides.inde
|
||||
import { Route as AdminRidesIndexRouteImport } from './routes/admin.rides.index'
|
||||
import { Route as AdminProductsIndexRouteImport } from './routes/admin.products.index'
|
||||
import { Route as AdminOrdersIndexRouteImport } from './routes/admin.orders.index'
|
||||
import { Route as PaymentsPaystackCallbackRouteImport } from './routes/payments.paystack.callback'
|
||||
import { Route as DriverRidesRequestedRouteImport } from './routes/driver.rides.requested'
|
||||
import { Route as DriverRidesCompletedRouteImport } from './routes/driver.rides.completed'
|
||||
import { Route as DriverRidesAcceptedRouteImport } from './routes/driver.rides.accepted'
|
||||
@@ -174,12 +173,6 @@ const AdminOrdersIndexRoute = AdminOrdersIndexRouteImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => AdminOrdersRoute,
|
||||
} as any)
|
||||
const PaymentsPaystackCallbackRoute =
|
||||
PaymentsPaystackCallbackRouteImport.update({
|
||||
id: '/payments/paystack/callback',
|
||||
path: '/payments/paystack/callback',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const DriverRidesRequestedRoute = DriverRidesRequestedRouteImport.update({
|
||||
id: '/requested',
|
||||
path: '/requested',
|
||||
@@ -246,7 +239,6 @@ export interface FileRoutesByFullPath {
|
||||
'/driver/rides/accepted': typeof DriverRidesAcceptedRoute
|
||||
'/driver/rides/completed': typeof DriverRidesCompletedRoute
|
||||
'/driver/rides/requested': typeof DriverRidesRequestedRoute
|
||||
'/payments/paystack/callback': typeof PaymentsPaystackCallbackRoute
|
||||
'/admin/orders/': typeof AdminOrdersIndexRoute
|
||||
'/admin/products/': typeof AdminProductsIndexRoute
|
||||
'/admin/rides/': typeof AdminRidesIndexRoute
|
||||
@@ -274,7 +266,6 @@ export interface FileRoutesByTo {
|
||||
'/driver/rides/accepted': typeof DriverRidesAcceptedRoute
|
||||
'/driver/rides/completed': typeof DriverRidesCompletedRoute
|
||||
'/driver/rides/requested': typeof DriverRidesRequestedRoute
|
||||
'/payments/paystack/callback': typeof PaymentsPaystackCallbackRoute
|
||||
'/admin/orders': typeof AdminOrdersIndexRoute
|
||||
'/admin/products': typeof AdminProductsIndexRoute
|
||||
'/admin/rides': typeof AdminRidesIndexRoute
|
||||
@@ -311,7 +302,6 @@ export interface FileRoutesById {
|
||||
'/driver/rides/accepted': typeof DriverRidesAcceptedRoute
|
||||
'/driver/rides/completed': typeof DriverRidesCompletedRoute
|
||||
'/driver/rides/requested': typeof DriverRidesRequestedRoute
|
||||
'/payments/paystack/callback': typeof PaymentsPaystackCallbackRoute
|
||||
'/admin/orders/': typeof AdminOrdersIndexRoute
|
||||
'/admin/products/': typeof AdminProductsIndexRoute
|
||||
'/admin/rides/': typeof AdminRidesIndexRoute
|
||||
@@ -349,7 +339,6 @@ export interface FileRouteTypes {
|
||||
| '/driver/rides/accepted'
|
||||
| '/driver/rides/completed'
|
||||
| '/driver/rides/requested'
|
||||
| '/payments/paystack/callback'
|
||||
| '/admin/orders/'
|
||||
| '/admin/products/'
|
||||
| '/admin/rides/'
|
||||
@@ -377,7 +366,6 @@ export interface FileRouteTypes {
|
||||
| '/driver/rides/accepted'
|
||||
| '/driver/rides/completed'
|
||||
| '/driver/rides/requested'
|
||||
| '/payments/paystack/callback'
|
||||
| '/admin/orders'
|
||||
| '/admin/products'
|
||||
| '/admin/rides'
|
||||
@@ -413,7 +401,6 @@ export interface FileRouteTypes {
|
||||
| '/driver/rides/accepted'
|
||||
| '/driver/rides/completed'
|
||||
| '/driver/rides/requested'
|
||||
| '/payments/paystack/callback'
|
||||
| '/admin/orders/'
|
||||
| '/admin/products/'
|
||||
| '/admin/rides/'
|
||||
@@ -428,7 +415,6 @@ export interface RootRouteChildren {
|
||||
ProductsRoute: typeof ProductsRouteWithChildren
|
||||
RidesRoute: typeof RidesRouteWithChildren
|
||||
DriverRidesRoute: typeof DriverRidesRouteWithChildren
|
||||
PaymentsPaystackCallbackRoute: typeof PaymentsPaystackCallbackRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
@@ -615,13 +601,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AdminOrdersIndexRouteImport
|
||||
parentRoute: typeof AdminOrdersRoute
|
||||
}
|
||||
'/payments/paystack/callback': {
|
||||
id: '/payments/paystack/callback'
|
||||
path: '/payments/paystack/callback'
|
||||
fullPath: '/payments/paystack/callback'
|
||||
preLoaderRoute: typeof PaymentsPaystackCallbackRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/driver/rides/requested': {
|
||||
id: '/driver/rides/requested'
|
||||
path: '/requested'
|
||||
@@ -809,7 +788,6 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
ProductsRoute: ProductsRouteWithChildren,
|
||||
RidesRoute: RidesRouteWithChildren,
|
||||
DriverRidesRoute: DriverRidesRouteWithChildren,
|
||||
PaymentsPaystackCallbackRoute: PaymentsPaystackCallbackRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
||||
@@ -14,10 +14,7 @@ import { Separator } from "#/components/ui/separator";
|
||||
import { formatCurrency } from "#/hooks/formatCurrency";
|
||||
import { useAuth } from "#/hooks/useAuth";
|
||||
import { useCart } from "#/hooks/useCart";
|
||||
import {
|
||||
useCheckoutQuote,
|
||||
useInitializePaystackPayment,
|
||||
} from "#/hooks/usePayments";
|
||||
import { useCheckoutQuote, useSimulateCheckout } from "#/hooks/usePayments";
|
||||
import { navigateToRoleHome } from "#/lib/auth-navigation";
|
||||
|
||||
export const Route = createFileRoute("/cart")({
|
||||
@@ -28,7 +25,7 @@ function CartPage() {
|
||||
const cart = useCart();
|
||||
const { user, login, register, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const initializePayment = useInitializePaystackPayment();
|
||||
const simulateCheckout = useSimulateCheckout();
|
||||
const [authMode, setAuthMode] = useState<"login" | "register" | null>(null);
|
||||
const [requestRide, setRequestRide] = useState(false);
|
||||
const [rideForm, setRideForm] = useState({
|
||||
@@ -125,11 +122,12 @@ function CartPage() {
|
||||
}
|
||||
|
||||
try {
|
||||
const payment = await initializePayment.mutateAsync(checkoutRequest);
|
||||
toast.success("Redirecting to Paystack", {
|
||||
description: `Payment ${payment.reference} is ready.`,
|
||||
const checkout = await simulateCheckout.mutateAsync(checkoutRequest);
|
||||
cart.clearCart();
|
||||
toast.success("Checkout complete", {
|
||||
description: `Order ${checkout.orderId.slice(0, 8)} is ready for fulfillment.`,
|
||||
});
|
||||
window.location.href = payment.authorizationUrl;
|
||||
await navigate({ to: "/orders" });
|
||||
} catch (error) {
|
||||
toast.error("Checkout failed", {
|
||||
description:
|
||||
@@ -326,14 +324,14 @@ function CartPage() {
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={initializePayment.isPending}
|
||||
disabled={simulateCheckout.isPending}
|
||||
onClick={checkout}
|
||||
className="w-full"
|
||||
>
|
||||
<CreditCard className="mr-2 size-4" />
|
||||
{initializePayment.isPending
|
||||
? "Opening Paystack..."
|
||||
: "Pay with Paystack"}
|
||||
{simulateCheckout.isPending
|
||||
? "Completing checkout..."
|
||||
: "Simulate checkout"}
|
||||
</Button>
|
||||
{!user ? (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
@@ -362,11 +360,11 @@ function CartPage() {
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{initializePayment.error ? (
|
||||
{simulateCheckout.error ? (
|
||||
<ErrorState
|
||||
message={
|
||||
initializePayment.error instanceof Error
|
||||
? initializePayment.error.message
|
||||
simulateCheckout.error instanceof Error
|
||||
? simulateCheckout.error.message
|
||||
: "Checkout failed"
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
|
||||
import { CheckCircle2, Loader2, XCircle } from "lucide-react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { SiteHeader } from "#/components/store/SiteHeader";
|
||||
import { Button } from "#/components/ui/button";
|
||||
import { Card, CardContent } from "#/components/ui/card";
|
||||
import { useAuth } from "#/hooks/useAuth";
|
||||
import { useCart } from "#/hooks/useCart";
|
||||
import { useVerifyPaystackPayment } from "#/hooks/usePayments";
|
||||
|
||||
export const Route = createFileRoute("/payments/paystack/callback")({
|
||||
validateSearch: (search: Record<string, unknown>) => ({
|
||||
reference: typeof search.reference === "string" ? search.reference : "",
|
||||
}),
|
||||
component: PaystackCallbackPage,
|
||||
});
|
||||
|
||||
function PaystackCallbackPage() {
|
||||
const { user, logout } = useAuth();
|
||||
const cart = useCart();
|
||||
const navigate = useNavigate();
|
||||
const { reference } = Route.useSearch();
|
||||
const verifyPayment = useVerifyPaystackPayment();
|
||||
const startedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!reference || startedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
startedRef.current = true;
|
||||
verifyPayment.mutate(reference, {
|
||||
onSuccess: (payment) => {
|
||||
if (payment.status === "PAID") {
|
||||
cart.clearCart();
|
||||
toast.success("Payment verified", {
|
||||
description: `Order ${payment.orderId.slice(0, 8)} is ready for fulfillment.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error("Payment not completed", {
|
||||
description: payment.message,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Payment verification failed", {
|
||||
description:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Please contact support with your reference.",
|
||||
});
|
||||
},
|
||||
});
|
||||
}, [cart, reference, verifyPayment]);
|
||||
|
||||
const paid = verifyPayment.data?.status === "PAID";
|
||||
const failed =
|
||||
!reference ||
|
||||
verifyPayment.isError ||
|
||||
(verifyPayment.data && verifyPayment.data.status !== "PAID");
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-background">
|
||||
<SiteHeader
|
||||
user={user}
|
||||
onLogin={() => navigate({ to: "/cart" })}
|
||||
onRegister={() => navigate({ to: "/cart" })}
|
||||
onLogout={logout}
|
||||
/>
|
||||
|
||||
<div className="mx-auto flex max-w-2xl px-4 py-12">
|
||||
<Card className="w-full">
|
||||
<CardContent className="space-y-5 p-6">
|
||||
{verifyPayment.isPending ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<Loader2 className="size-5 animate-spin text-muted-foreground" />
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">Verifying payment</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Reference {reference}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{paid ? (
|
||||
<div className="rounded-md border p-4">
|
||||
<div className="flex gap-3">
|
||||
<CheckCircle2 className="mt-0.5 size-4 text-emerald-600" />
|
||||
<div>
|
||||
<h1 className="font-semibold">Payment verified</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Order {verifyPayment.data?.orderId.slice(0, 8)} is pending
|
||||
fulfillment.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{failed ? (
|
||||
<div className="rounded-md border border-destructive/40 p-4 text-destructive">
|
||||
<div className="flex gap-3">
|
||||
<XCircle className="mt-0.5 size-4" />
|
||||
<div>
|
||||
<h1 className="font-semibold">
|
||||
Payment could not be verified
|
||||
</h1>
|
||||
<p className="text-sm">
|
||||
{reference
|
||||
? (verifyPayment.data?.message ??
|
||||
verifyPayment.error?.message ??
|
||||
"Please contact support with your reference.")
|
||||
: "The Paystack callback did not include a reference."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Button asChild>
|
||||
<Link to="/orders">View orders</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline">
|
||||
<Link to="/cart">Back to cart</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -10,25 +10,14 @@ export type CheckoutQuote = {
|
||||
expiresAt: string;
|
||||
};
|
||||
|
||||
export type PaystackInitializeRequest = CreateOrderRequest;
|
||||
export type SimulatedCheckoutRequest = CreateOrderRequest;
|
||||
|
||||
export type PaystackInitializeResponse = {
|
||||
authorizationUrl: string;
|
||||
accessCode: string;
|
||||
export type SimulatedCheckoutResponse = {
|
||||
reference: string;
|
||||
orderId: string;
|
||||
rideId: string | null;
|
||||
amount: number;
|
||||
currency: string;
|
||||
status: "PENDING" | "PAID" | "FAILED" | "ABANDONED" | "REFUNDED";
|
||||
};
|
||||
|
||||
export type PaystackVerifyResponse = {
|
||||
reference: string;
|
||||
status: "PENDING" | "PAID" | "FAILED" | "ABANDONED" | "REFUNDED";
|
||||
orderId: string;
|
||||
rideId: string | null;
|
||||
amount: number;
|
||||
currency: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user