purge paystack

This commit is contained in:
2026-06-15 11:38:04 +03:00
parent 8f7cec33ef
commit 6a98e8984e
26 changed files with 85 additions and 730 deletions
+7 -20
View File
@@ -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,
});
}
+5 -24
View File
@@ -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 }),
-22
View File
@@ -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 -16
View File
@@ -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>
);
}
+2 -13
View File
@@ -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;
};