mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-10-05 09:55:25 +00:00
refactor: create utility function to hide AccountInformation component for topup and payment 🔧
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 8m50s
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 8m50s
This commit is contained in:
@@ -9,72 +9,72 @@ import { TextShimmer } from "@/components/ui/text-shimmer";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { tryCatch } from "@/utils/tryCatch";
|
import { tryCatch } from "@/utils/tryCatch";
|
||||||
export default async function TopupPage({
|
export default async function TopupPage({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ topupId: string }>;
|
params: Promise<{ topupId: string }>;
|
||||||
}) {
|
}) {
|
||||||
const topupId = (await params).topupId;
|
const topupId = (await params).topupId;
|
||||||
const [error, topup] = await tryCatch(getTopup({ id: topupId }));
|
const [error, topup] = await tryCatch(getTopup({ id: topupId }));
|
||||||
if (error) {
|
if (error) {
|
||||||
if (error.message === "Invalid token.") redirect("/auth/signin");
|
if (error.message === "Invalid token.") redirect("/auth/signin");
|
||||||
return <ClientErrorMessage message={error.message} />;
|
return <ClientErrorMessage message={error.message} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-4 mb-4 mx-2">
|
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-4 mb-4 mx-2">
|
||||||
<h3 className="text-sarLinkOrange text-2xl">Topup</h3>
|
<h3 className="text-sarLinkOrange text-2xl">Topup</h3>
|
||||||
<div className="flex flex-col gap-4 items-end w-full">
|
<div className="flex flex-col gap-4 items-end w-full">
|
||||||
{!topup.is_expired && topup.paid && topup.status !== "PENDING" && (
|
{!topup.is_expired && topup.paid && topup.status !== "PENDING" && (
|
||||||
<Button
|
<Button
|
||||||
disabled
|
disabled
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-md opacity-100! uppercase font-semibold",
|
"rounded-md opacity-100! uppercase font-semibold",
|
||||||
topup?.paid
|
topup?.paid
|
||||||
? "text-green-900 bg-green-500/20"
|
? "text-green-900 bg-green-500/20"
|
||||||
: "text-inherit bg-yellow-400",
|
: "text-inherit bg-yellow-400",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{topup.status}
|
{topup.status}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{topup.status === "PENDING" && !topup.is_expired && (
|
{topup.status === "PENDING" && !topup.is_expired && (
|
||||||
<Button>
|
<Button>
|
||||||
<TextShimmer>Payment Pending</TextShimmer>{" "}
|
<TextShimmer>Payment Pending</TextShimmer>{" "}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!topup.paid &&
|
{!topup.paid &&
|
||||||
(topup.is_expired ? (
|
(topup.is_expired ? (
|
||||||
<Button
|
<Button
|
||||||
disabled
|
disabled
|
||||||
className="rounded-md opacity-100! uppercase font-semibold text-red-500 bg-red-500/20"
|
className="rounded-md opacity-100! uppercase font-semibold text-red-500 bg-red-500/20"
|
||||||
>
|
>
|
||||||
Topup Expired
|
Topup Expired
|
||||||
</Button>
|
</Button>
|
||||||
) : topup.status === "PENDING" ? (
|
) : topup.status === "PENDING" ? (
|
||||||
<CancelTopupButton topupId={topupId} />
|
<CancelTopupButton topupId={topupId} />
|
||||||
) : topup.status === "CANCELLED" ? (
|
) : topup.status === "CANCELLED" ? (
|
||||||
<Button disabled>Topup Cancelled</Button>
|
<Button disabled>Topup Cancelled</Button>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!topup.paid && topup.status === "PENDING" && !topup.is_expired && (
|
{!topup.paid && topup.status === "PENDING" && !topup.is_expired && (
|
||||||
<ExpiryCountDown expiryLabel="Top up" expiresAt={topup.expires_at} />
|
<ExpiryCountDown expiryLabel="Top up" expiresAt={topup.expires_at} />
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
id="user-topup-details"
|
id="user-topup-details"
|
||||||
className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
|
className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
|
||||||
>
|
>
|
||||||
<TopupToPay
|
<TopupToPay
|
||||||
disabled={
|
disabled={
|
||||||
topup.paid || topup.is_expired || topup.status === "CANCELLED"
|
topup.paid || topup.is_expired || topup.status === "CANCELLED"
|
||||||
}
|
}
|
||||||
topup={topup || undefined}
|
topup={topup || undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -3,198 +3,201 @@ import { BadgeDollarSign, Loader2, Wallet } from "lucide-react";
|
|||||||
import { useActionState, useEffect } from "react";
|
import { useActionState, useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
type VerifyDevicePaymentState,
|
type VerifyDevicePaymentState,
|
||||||
verifyDevicePayment,
|
verifyDevicePayment,
|
||||||
} from "@/actions/payment";
|
} from "@/actions/payment";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCaption,
|
TableCaption,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableFooter,
|
TableFooter,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import type { Payment } from "@/lib/backend-types";
|
import type { Payment } from "@/lib/backend-types";
|
||||||
import type { User } from "@/lib/types/user";
|
import type { User } from "@/lib/types/user";
|
||||||
|
import { shouldShowPaymentInfo } from "@/lib/utils";
|
||||||
import { AccountInfomation } from "./account-information";
|
import { AccountInfomation } from "./account-information";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
const initialState: VerifyDevicePaymentState = {
|
const initialState: VerifyDevicePaymentState = {
|
||||||
message: "",
|
message: "",
|
||||||
success: false,
|
success: false,
|
||||||
fieldErrors: {},
|
fieldErrors: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DevicesToPay({
|
export default function DevicesToPay({
|
||||||
payment,
|
payment,
|
||||||
user,
|
user,
|
||||||
disabled,
|
disabled,
|
||||||
}: {
|
}: {
|
||||||
payment?: Payment;
|
payment?: Payment;
|
||||||
user?: User;
|
user?: User;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [state, formAction, isPending] = useActionState(
|
const [state, formAction, isPending] = useActionState(
|
||||||
verifyDevicePayment,
|
verifyDevicePayment,
|
||||||
initialState,
|
initialState,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle toast notifications based on state changes
|
// Handle toast notifications based on state changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.success && state.message) {
|
if (state.success && state.message) {
|
||||||
toast.success("Payment successful!", {
|
toast.success("Payment successful!", {
|
||||||
closeButton: true,
|
closeButton: true,
|
||||||
description: state.message,
|
description: state.message,
|
||||||
});
|
});
|
||||||
} else if (
|
} else if (
|
||||||
!state.success &&
|
!state.success &&
|
||||||
state.message &&
|
state.message &&
|
||||||
state.message !== initialState.message
|
state.message !== initialState.message
|
||||||
) {
|
) {
|
||||||
toast.error("Payment Verification Failed", {
|
toast.error("Payment Verification Failed", {
|
||||||
closeButton: true,
|
closeButton: true,
|
||||||
description: state.message,
|
description: state.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
const devices = payment?.devices;
|
const devices = payment?.devices;
|
||||||
if (devices?.length === 0) {
|
if (devices?.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// 100+(n−1)×75
|
// 100+(n−1)×75
|
||||||
const walletBalance = user?.wallet_balance ?? 0;
|
const walletBalance = user?.wallet_balance ?? 0;
|
||||||
|
|
||||||
const isWalletPayVisible = walletBalance > (payment?.amount ?? 0);
|
const isWalletPayVisible = walletBalance > (payment?.amount ?? 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="p-2 flex flex-col gap-2">
|
<div className="p-2 flex flex-col gap-2">
|
||||||
<h3 className="title-bg my-1 p-2 border border-dashed rounded-md font-semibold text-lg">
|
<h3 className="title-bg my-1 p-2 border border-dashed rounded-md font-semibold text-lg">
|
||||||
{!payment?.paid ? "Devices to pay" : "Devices Paid"}
|
{!payment?.paid ? "Devices to pay" : "Devices Paid"}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{devices?.map((device) => (
|
{devices?.map((device) => (
|
||||||
<div
|
<div
|
||||||
key={device.id}
|
key={device.id}
|
||||||
className="bg-muted border rounded p-2 flex gap-2 items-center"
|
className="bg-muted border rounded p-2 flex gap-2 items-center"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="text-sm font-medium">{device.name}</div>
|
<div className="text-sm font-medium">{device.name}</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
{device.mac}
|
{device.mac}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="m-2 flex items-end justify-end p-2 text-sm text-foreground border rounded">
|
<div className="m-2 flex items-end justify-end p-2 text-sm text-foreground border rounded">
|
||||||
<Table>
|
<Table>
|
||||||
<TableCaption>
|
<TableCaption>
|
||||||
<div className="max-w-sm mx-auto">
|
{shouldShowPaymentInfo(payment) && (
|
||||||
<p>Please send the following amount to the payment address</p>
|
<div className="max-w-sm mx-auto">
|
||||||
<AccountInfomation
|
<p>Please send the following amount to the payment address</p>
|
||||||
accName="Baraveli Dev"
|
<AccountInfomation
|
||||||
accountNo="90101400028321000"
|
accName="Baraveli Dev"
|
||||||
/>
|
accountNo="90101400028321000"
|
||||||
{payment?.paid ? (
|
/>
|
||||||
<Button
|
{payment?.paid ? (
|
||||||
size={"lg"}
|
<Button
|
||||||
variant={"secondary"}
|
size={"lg"}
|
||||||
disabled
|
variant={"secondary"}
|
||||||
className="dark:text-green-200 text-green-900 bg-green-500/20 uppercase font-semibold"
|
disabled
|
||||||
>
|
className="dark:text-green-200 text-green-900 bg-green-500/20 uppercase font-semibold"
|
||||||
Payment Verified
|
>
|
||||||
</Button>
|
Payment Verified
|
||||||
) : (
|
</Button>
|
||||||
<div className="flex flex-col gap-2">
|
) : (
|
||||||
{isWalletPayVisible && (
|
<div className="flex flex-col gap-2">
|
||||||
<form action={formAction}>
|
{isWalletPayVisible && (
|
||||||
<input
|
<form action={formAction}>
|
||||||
type="hidden"
|
<input
|
||||||
name="paymentId"
|
type="hidden"
|
||||||
value={payment?.id ?? ""}
|
name="paymentId"
|
||||||
/>
|
value={payment?.id ?? ""}
|
||||||
<input type="hidden" name="method" value="WALLET" />
|
/>
|
||||||
<Button
|
<input type="hidden" name="method" value="WALLET" />
|
||||||
disabled={isPending}
|
<Button
|
||||||
type="submit"
|
disabled={isPending}
|
||||||
variant={"secondary"}
|
type="submit"
|
||||||
size={"lg"}
|
variant={"secondary"}
|
||||||
>
|
size={"lg"}
|
||||||
{isPending
|
>
|
||||||
? "Processing payment..."
|
{isPending
|
||||||
: "Pay with wallet"}
|
? "Processing payment..."
|
||||||
<Wallet />
|
: "Pay with wallet"}
|
||||||
</Button>
|
<Wallet />
|
||||||
</form>
|
</Button>
|
||||||
)}
|
</form>
|
||||||
<form action={formAction}>
|
)}
|
||||||
<input
|
<form action={formAction}>
|
||||||
type="hidden"
|
<input
|
||||||
name="paymentId"
|
type="hidden"
|
||||||
value={payment?.id ?? ""}
|
name="paymentId"
|
||||||
/>
|
value={payment?.id ?? ""}
|
||||||
<input type="hidden" name="method" value="TRANSFER" />
|
/>
|
||||||
<Button
|
<input type="hidden" name="method" value="TRANSFER" />
|
||||||
disabled={isPending || disabled}
|
<Button
|
||||||
type="submit"
|
disabled={isPending || disabled}
|
||||||
size={"lg"}
|
type="submit"
|
||||||
className="mb-4"
|
size={"lg"}
|
||||||
>
|
className="mb-4"
|
||||||
{isPending ? "Processing payment..." : "I have paid"}
|
>
|
||||||
{isPending ? (
|
{isPending ? "Processing payment..." : "I have paid"}
|
||||||
<Loader2 className="animate-spin" />
|
{isPending ? (
|
||||||
) : (
|
<Loader2 className="animate-spin" />
|
||||||
<BadgeDollarSign />
|
) : (
|
||||||
)}
|
<BadgeDollarSign />
|
||||||
</Button>
|
)}
|
||||||
</form>
|
</Button>
|
||||||
</div>
|
</form>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</TableCaption>
|
</div>
|
||||||
<TableBody className="">
|
)}
|
||||||
<TableRow>
|
</TableCaption>
|
||||||
<TableCell>Payment created</TableCell>
|
<TableBody className="">
|
||||||
<TableCell className="text-right">
|
<TableRow>
|
||||||
{new Date(payment?.created_at ?? "").toLocaleDateString(
|
<TableCell>Payment created</TableCell>
|
||||||
"en-US",
|
<TableCell className="text-right">
|
||||||
{
|
{new Date(payment?.created_at ?? "").toLocaleDateString(
|
||||||
month: "short",
|
"en-US",
|
||||||
day: "2-digit",
|
{
|
||||||
year: "numeric",
|
month: "short",
|
||||||
minute: "2-digit",
|
day: "2-digit",
|
||||||
hour: "2-digit",
|
year: "numeric",
|
||||||
second: "2-digit",
|
minute: "2-digit",
|
||||||
},
|
hour: "2-digit",
|
||||||
)}
|
second: "2-digit",
|
||||||
</TableCell>
|
},
|
||||||
</TableRow>
|
)}
|
||||||
<TableRow>
|
</TableCell>
|
||||||
<TableCell>Total Devices</TableCell>
|
</TableRow>
|
||||||
<TableCell className="text-right text-xl">
|
<TableRow>
|
||||||
{devices?.length}
|
<TableCell>Total Devices</TableCell>
|
||||||
</TableCell>
|
<TableCell className="text-right text-xl">
|
||||||
</TableRow>
|
{devices?.length}
|
||||||
<TableRow>
|
</TableCell>
|
||||||
<TableCell>Duration</TableCell>
|
</TableRow>
|
||||||
<TableCell className="text-right text-xl">
|
<TableRow>
|
||||||
{payment?.number_of_months} Months
|
<TableCell>Duration</TableCell>
|
||||||
</TableCell>
|
<TableCell className="text-right text-xl">
|
||||||
</TableRow>
|
{payment?.number_of_months} Months
|
||||||
</TableBody>
|
</TableCell>
|
||||||
<TableFooter>
|
</TableRow>
|
||||||
<TableRow className="">
|
</TableBody>
|
||||||
<TableCell colSpan={1}>Total Due</TableCell>
|
<TableFooter>
|
||||||
<TableCell className="text-right text-3xl font-bold">
|
<TableRow className="">
|
||||||
{payment?.amount?.toFixed(2)}
|
<TableCell colSpan={1}>Total Due</TableCell>
|
||||||
</TableCell>
|
<TableCell className="text-right text-3xl font-bold">
|
||||||
</TableRow>
|
{payment?.amount?.toFixed(2)}
|
||||||
</TableFooter>
|
</TableCell>
|
||||||
</Table>
|
</TableRow>
|
||||||
</div>
|
</TableFooter>
|
||||||
</div>
|
</Table>
|
||||||
);
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@@ -3,155 +3,154 @@ import { BadgeDollarSign, Loader2 } from "lucide-react";
|
|||||||
import { useActionState, useEffect } from "react";
|
import { useActionState, useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
type VerifyTopupPaymentState,
|
type VerifyTopupPaymentState,
|
||||||
verifyTopupPayment,
|
verifyTopupPayment,
|
||||||
} from "@/actions/payment";
|
} from "@/actions/payment";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCaption,
|
TableCaption,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableFooter,
|
TableFooter,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import type { Topup } from "@/lib/backend-types";
|
import type { Topup } from "@/lib/backend-types";
|
||||||
|
import { shouldShowTopupPaymentInfo } from "@/lib/utils";
|
||||||
import { AccountInfomation } from "./account-information";
|
import { AccountInfomation } from "./account-information";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
const initialState: VerifyTopupPaymentState = {
|
const initialState: VerifyTopupPaymentState = {
|
||||||
message: "",
|
message: "",
|
||||||
success: false,
|
success: false,
|
||||||
fieldErrors: {},
|
fieldErrors: {},
|
||||||
};
|
};
|
||||||
export default function TopupToPay({
|
export default function TopupToPay({
|
||||||
topup,
|
topup,
|
||||||
disabled,
|
disabled,
|
||||||
}: {
|
}: {
|
||||||
topup?: Topup;
|
topup?: Topup;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [state, formAction, isPending] = useActionState(
|
const [state, formAction, isPending] = useActionState(
|
||||||
verifyTopupPayment,
|
verifyTopupPayment,
|
||||||
initialState,
|
initialState,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle toast notifications based on state changes
|
// Handle toast notifications based on state changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.success && state.message) {
|
if (state.success && state.message) {
|
||||||
toast.success("Topup successful!", {
|
toast.success("Topup successful!", {
|
||||||
closeButton: true,
|
closeButton: true,
|
||||||
description: state.transaction
|
description: state.transaction
|
||||||
? `Your topup payment has been verified successfully using ${state.transaction.sourceBank} bank transfer on ${state.transaction.trxDate}.`
|
? `Your topup payment has been verified successfully using ${state.transaction.sourceBank} bank transfer on ${state.transaction.trxDate}.`
|
||||||
: state.message,
|
: state.message,
|
||||||
});
|
});
|
||||||
} else if (
|
} else if (
|
||||||
!state.success &&
|
!state.success &&
|
||||||
state.message &&
|
state.message &&
|
||||||
state.message !== initialState.message
|
state.message !== initialState.message
|
||||||
) {
|
) {
|
||||||
toast.error("Topup Payment Verification Failed", {
|
toast.error("Topup Payment Verification Failed", {
|
||||||
closeButton: true,
|
closeButton: true,
|
||||||
description: state.message,
|
description: state.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="m-2 flex items-end justify-end p-2 text-sm text-foreground border rounded">
|
<div className="m-2 flex items-end justify-end p-2 text-sm text-foreground border rounded">
|
||||||
<Table>
|
<Table>
|
||||||
<TableCaption>
|
<TableCaption>
|
||||||
{(!topup?.paid ||
|
{shouldShowTopupPaymentInfo(topup) && (
|
||||||
!topup?.is_expired ||
|
<div className="max-w-sm mx-auto">
|
||||||
topup?.status !== "CANCELLED") && (
|
<p>Please send the following amount to the payment address</p>
|
||||||
<div className="max-w-sm mx-auto">
|
<AccountInfomation
|
||||||
<p>Please send the following amount to the payment address</p>
|
accName="Baraveli Dev"
|
||||||
<AccountInfomation
|
accountNo="90101400028321000"
|
||||||
accName="Baraveli Dev"
|
/>
|
||||||
accountNo="90101400028321000"
|
{topup?.paid ? (
|
||||||
/>
|
<Button
|
||||||
{topup?.paid ? (
|
size={"lg"}
|
||||||
<Button
|
variant={"secondary"}
|
||||||
size={"lg"}
|
disabled
|
||||||
variant={"secondary"}
|
className="dark:text-green-200 text-green-900 bg-green-500/20 uppercase font-semibold"
|
||||||
disabled
|
>
|
||||||
className="dark:text-green-200 text-green-900 bg-green-500/20 uppercase font-semibold"
|
Topup Payment Verified
|
||||||
>
|
</Button>
|
||||||
Topup Payment Verified
|
) : (
|
||||||
</Button>
|
<div className="flex flex-col gap-2">
|
||||||
) : (
|
<form action={formAction}>
|
||||||
<div className="flex flex-col gap-2">
|
<input
|
||||||
<form action={formAction}>
|
type="hidden"
|
||||||
<input
|
name="topupId"
|
||||||
type="hidden"
|
value={topup?.id ?? ""}
|
||||||
name="topupId"
|
/>
|
||||||
value={topup?.id ?? ""}
|
<Button
|
||||||
/>
|
disabled={disabled || isPending}
|
||||||
<Button
|
type="submit"
|
||||||
disabled={disabled || isPending}
|
size={"lg"}
|
||||||
type="submit"
|
className="mb-4 w-full"
|
||||||
size={"lg"}
|
>
|
||||||
className="mb-4 w-full"
|
{isPending ? "Processing payment..." : "I have paid"}
|
||||||
>
|
{isPending ? (
|
||||||
{isPending ? "Processing payment..." : "I have paid"}
|
<Loader2 className="animate-spin" />
|
||||||
{isPending ? (
|
) : (
|
||||||
<Loader2 className="animate-spin" />
|
<BadgeDollarSign />
|
||||||
) : (
|
)}
|
||||||
<BadgeDollarSign />
|
</Button>
|
||||||
)}
|
</form>
|
||||||
</Button>
|
</div>
|
||||||
</form>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</TableCaption>
|
||||||
)}
|
<TableBody className="">
|
||||||
</TableCaption>
|
<TableRow>
|
||||||
<TableBody className="">
|
<TableCell>Topup created</TableCell>
|
||||||
<TableRow>
|
<TableCell className="text-right text-muted-foreground">
|
||||||
<TableCell>Topup created</TableCell>
|
{new Date(topup?.created_at ?? "").toLocaleDateString("en-US", {
|
||||||
<TableCell className="text-right text-muted-foreground">
|
month: "short",
|
||||||
{new Date(topup?.created_at ?? "").toLocaleDateString("en-US", {
|
day: "2-digit",
|
||||||
month: "short",
|
year: "numeric",
|
||||||
day: "2-digit",
|
minute: "2-digit",
|
||||||
year: "numeric",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
second: "2-digit",
|
||||||
hour: "2-digit",
|
})}
|
||||||
second: "2-digit",
|
</TableCell>
|
||||||
})}
|
</TableRow>
|
||||||
</TableCell>
|
<TableRow>
|
||||||
</TableRow>
|
<TableCell>Payment received</TableCell>
|
||||||
<TableRow>
|
<TableCell className="text-right text-sarLinkOrange">
|
||||||
<TableCell>Payment received</TableCell>
|
{topup?.paid_at
|
||||||
<TableCell className="text-right text-sarLinkOrange">
|
? new Date(topup.paid_at).toLocaleDateString("en-US", {
|
||||||
{topup?.paid_at
|
month: "short",
|
||||||
? new Date(topup.paid_at).toLocaleDateString("en-US", {
|
day: "2-digit",
|
||||||
month: "short",
|
year: "numeric",
|
||||||
day: "2-digit",
|
minute: "2-digit",
|
||||||
year: "numeric",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
second: "2-digit",
|
||||||
hour: "2-digit",
|
})
|
||||||
second: "2-digit",
|
: "-"}
|
||||||
})
|
</TableCell>
|
||||||
: "-"}
|
</TableRow>
|
||||||
</TableCell>
|
<TableRow>
|
||||||
</TableRow>
|
<TableCell>MIB Reference</TableCell>
|
||||||
<TableRow>
|
<TableCell className="text-right">
|
||||||
<TableCell>MIB Reference</TableCell>
|
{topup?.mib_reference ? topup.mib_reference : "-"}
|
||||||
<TableCell className="text-right">
|
</TableCell>
|
||||||
{topup?.mib_reference ? topup.mib_reference : "-"}
|
</TableRow>
|
||||||
</TableCell>
|
</TableBody>
|
||||||
</TableRow>
|
<TableFooter>
|
||||||
</TableBody>
|
<TableRow className="">
|
||||||
<TableFooter>
|
<TableCell colSpan={1}>Total Due</TableCell>
|
||||||
<TableRow className="">
|
<TableCell className="text-right text-3xl font-bold">
|
||||||
<TableCell colSpan={1}>Total Due</TableCell>
|
{topup?.amount?.toFixed(2)}
|
||||||
<TableCell className="text-right text-3xl font-bold">
|
</TableCell>
|
||||||
{topup?.amount?.toFixed(2)}
|
</TableRow>
|
||||||
</TableCell>
|
</TableFooter>
|
||||||
</TableRow>
|
</Table>
|
||||||
</TableFooter>
|
</div>
|
||||||
</Table>
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
78
lib/utils.ts
78
lib/utils.ts
@@ -1,48 +1,72 @@
|
|||||||
import { type ClassValue, clsx } from "clsx";
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import type { Payment, Topup } from "./backend-types";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatDate = (date: Date): string => {
|
export const formatDate = (date: Date): string => {
|
||||||
const pad = (num: number): string => num.toString().padStart(2, "0");
|
const pad = (num: number): string => num.toString().padStart(2, "0");
|
||||||
|
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const month = pad(date.getMonth() + 1); // Months are zero-based
|
const month = pad(date.getMonth() + 1); // Months are zero-based
|
||||||
const day = pad(date.getDate());
|
const day = pad(date.getDate());
|
||||||
const hours = pad(date.getHours());
|
const hours = pad(date.getHours());
|
||||||
const minutes = pad(date.getMinutes() + 5);
|
const minutes = pad(date.getMinutes() + 5);
|
||||||
|
|
||||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatMacAddress = (mac: string): string => {
|
export const formatMacAddress = (mac: string): string => {
|
||||||
const formatted = mac
|
const formatted = mac
|
||||||
.replace(/[^A-Fa-f0-9]/g, "")
|
.replace(/[^A-Fa-f0-9]/g, "")
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
.match(/.{2}/g);
|
.match(/.{2}/g);
|
||||||
|
|
||||||
// Provide a fallback if formatted is null
|
// Provide a fallback if formatted is null
|
||||||
return formatted ? formatted.join("-") : "";
|
return formatted ? formatted.join("-") : "";
|
||||||
};
|
};
|
||||||
|
|
||||||
export function validateApiKey(request: Request) {
|
export function validateApiKey(request: Request) {
|
||||||
// Get API key from environment variable
|
// Get API key from environment variable
|
||||||
const validApiKey = process.env.CRON_API_KEY;
|
const validApiKey = process.env.CRON_API_KEY;
|
||||||
|
|
||||||
if (!validApiKey) {
|
if (!validApiKey) {
|
||||||
throw new Error("CRON_API_KEY is not configured");
|
throw new Error("CRON_API_KEY is not configured");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get API key from request header
|
// Get API key from request header
|
||||||
const apiKey = request.headers.get("x-api-key");
|
const apiKey = request.headers.get("x-api-key");
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new Error("API key is missing");
|
throw new Error("API key is missing");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (apiKey !== validApiKey) {
|
if (apiKey !== validApiKey) {
|
||||||
throw new Error("Invalid API key");
|
throw new Error("Invalid API key");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldShowTopupPaymentInfo(topup: Topup | undefined): boolean {
|
||||||
|
if (!topup) return false;
|
||||||
|
|
||||||
|
return !(
|
||||||
|
topup.paid ||
|
||||||
|
topup.is_expired ||
|
||||||
|
topup.status === "CANCELLED" ||
|
||||||
|
topup.status === "VERIFIED"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function shouldShowPaymentInfo(topup: Payment | undefined): boolean {
|
||||||
|
if (!topup) return false;
|
||||||
|
|
||||||
|
return !(
|
||||||
|
topup.paid ||
|
||||||
|
topup.is_expired ||
|
||||||
|
topup.status === "CANCELLED" ||
|
||||||
|
topup.status === "PAID"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user