Merge pull request #14 from i701/feat/device-payments
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 6m26s

refactor: implement device payment verification with useActionState hook 🔨
This commit is contained in:
Abdulla Aidhaan
2025-07-06 23:10:04 +05:00
committed by GitHub
2 changed files with 131 additions and 50 deletions

View File

@ -218,6 +218,84 @@ export async function verifyPayment({ id, method }: UpdatePayment) {
return handleApiResponse<Payment>(response, "updatePayment"); return handleApiResponse<Payment>(response, "updatePayment");
} }
export type VerifyDevicePaymentState = {
payment?: Payment;
message: string;
success: boolean;
fieldErrors: Record<string, string>;
payload?: FormData;
}
export async function verifyDevicePayment(
_prevState: VerifyDevicePaymentState,
formData: FormData
): Promise<VerifyDevicePaymentState> {
const session = await getServerSession(authOptions);
// Get the payment ID and method from the form data
const paymentId = formData.get('paymentId') as string;
const method = formData.get('method') as "TRANSFER" | "WALLET";
if (!paymentId) {
return {
message: "Payment ID is required",
success: false,
fieldErrors: { paymentId: "Payment ID is required" },
};
}
if (!method) {
return {
message: "Payment method is required",
success: false,
fieldErrors: { method: "Payment method is required" },
};
}
try {
const response = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/billing/payment/${paymentId}/verify/`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Token ${session?.apiToken}`,
},
body: JSON.stringify({
method,
}),
},
);
const result = await handleApiResponse<Payment>(response, "verifyDevicePayment");
revalidatePath("/payments/[paymentId]", "page");
return {
message: method === "WALLET"
? "Payment completed successfully using wallet!"
: "Payment verification successful!",
success: true,
fieldErrors: {},
payment: result,
};
} catch (error: unknown) {
if (error instanceof Error) {
return {
message: error.message || "Payment verification failed. Please try again.",
success: false,
fieldErrors: {},
};
} else {
return {
message: "Payment verification failed.",
success: false,
fieldErrors: {},
};
}
}
}
export type VerifyTopupPaymentState = { export type VerifyTopupPaymentState = {
transaction?: { transaction?: {

View File

@ -6,9 +6,9 @@ import {
Loader2, Loader2,
Wallet, Wallet,
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { useActionState, useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { verifyPayment } from "@/actions/payment"; import { type VerifyDevicePaymentState, verifyDevicePayment } from "@/actions/payment";
import { import {
Table, Table,
TableBody, TableBody,
@ -21,13 +21,32 @@ import type { Payment } from "@/lib/backend-types";
import type { User } from "@/lib/types/user"; import type { User } from "@/lib/types/user";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
const initialState: VerifyDevicePaymentState = {
message: "",
success: false,
fieldErrors: {},
};
export default function DevicesToPay({ export default function DevicesToPay({
payment, payment,
user, user,
}: { payment?: Payment; user?: User }) { }: { payment?: Payment; user?: User }) {
const [verifyingWalletPayment, setVerifyingWalletPayment] = useState(false); const [state, formAction, isPending] = useActionState(verifyDevicePayment, initialState);
const [verifyingTransferPayment, setVerifyingTransferPayment] = useState(false);
// Handle toast notifications based on state changes
useEffect(() => {
if (state.success && state.message) {
toast.success("Payment successful!", {
closeButton: true,
description: state.message,
});
} else if (!state.success && state.message && state.message !== initialState.message) {
toast.error("Payment Verification Failed", {
closeButton: true,
description: state.message,
});
}
}, [state]);
const devices = payment?.devices; const devices = payment?.devices;
if (devices?.length === 0) { if (devices?.length === 0) {
@ -81,53 +100,37 @@ export default function DevicesToPay({
) : ( ) : (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{isWalletPayVisible && ( {isWalletPayVisible && (
<Button <form action={formAction}>
disabled={verifyingWalletPayment || verifyingTransferPayment} <input type="hidden" name="paymentId" value={payment?.id ?? ""} />
onClick={async () => { <input type="hidden" name="method" value="WALLET" />
setVerifyingWalletPayment(true); <Button
await verifyPayment({ disabled={isPending}
method: "WALLET", type="submit"
id: payment?.id ?? "", variant={"secondary"}
}); size={"lg"}
setVerifyingWalletPayment(false); >
}} {isPending ? "Processing payment..." : "Pay with wallet"}
variant={"secondary"} <Wallet />
size={"lg"} </Button>
> </form>
{verifyingWalletPayment ? "Processing payment..." : "Pay with wallet"}
<Wallet />
</Button>
)} )}
<Button <form action={formAction}>
disabled={verifyingTransferPayment || verifyingWalletPayment} <input type="hidden" name="paymentId" value={payment?.id ?? ""} />
onClick={async () => { <input type="hidden" name="method" value="TRANSFER" />
setVerifyingTransferPayment(true); <Button
try { disabled={isPending}
await verifyPayment({ type="submit"
id: payment?.id ?? "", size={"lg"}
method: "TRANSFER" className="mb-4"
}); >
toast.success("Payment verification successful!"); {isPending ? "Processing payment..." : "I have paid"}
} catch (error: unknown) { {isPending ? (
if (error instanceof Error) { <Loader2 className="animate-spin" />
toast.error(error.message); ) : (
} else { <BadgeDollarSign />
toast.error("Payment verification failed."); )}
} </Button>
} finally { </form>
setVerifyingTransferPayment(false);
}
}}
size={"lg"}
className="mb-4"
>
{verifyingTransferPayment ? "Processing payment..." : "I have paid"}
{verifyingTransferPayment ? (
<Loader2 className="animate-spin" />
) : (
<BadgeDollarSign />
)}
</Button>
</div> </div>
)} )}
</div> </div>