197 lines
5.9 KiB
TypeScript
197 lines
5.9 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
|
|
import { PaymentMethod, VmInstance, VmPayment } from "../api";
|
|
import VpsPayment from "../components/vps-payment";
|
|
import useLogin from "../hooks/login";
|
|
import { AsyncButton } from "../components/button";
|
|
import CostLabel, { CostAmount } from "../components/cost";
|
|
import { RevolutPayWidget } from "../components/revolut";
|
|
import { timeValue } from "../utils";
|
|
|
|
export function VmBillingPage() {
|
|
const location = useLocation() as { state?: VmInstance };
|
|
const params = useParams();
|
|
const login = useLogin();
|
|
const navigate = useNavigate();
|
|
const [methods, setMethods] = useState<Array<PaymentMethod>>();
|
|
const [method, setMethod] = useState<PaymentMethod>();
|
|
const [payment, setPayment] = useState<VmPayment>();
|
|
const [payments, setPayments] = useState<Array<VmPayment>>([]);
|
|
const [state, setState] = useState<VmInstance | undefined>(location?.state);
|
|
|
|
async function listPayments() {
|
|
if (!state) return;
|
|
const history = await login?.api.listPayments(state.id);
|
|
setPayments(history ?? []);
|
|
}
|
|
|
|
async function reloadVmState() {
|
|
if (!state) return;
|
|
const newState = await login?.api.getVm(state.id);
|
|
setState(newState);
|
|
setMethod(undefined);
|
|
setMethods(undefined);
|
|
return newState;
|
|
}
|
|
|
|
async function onPaid() {
|
|
setMethod(undefined);
|
|
setMethods(undefined);
|
|
const s = reloadVmState();
|
|
if (params["action"] === "renew") {
|
|
navigate("/vm", { state: s });
|
|
}
|
|
}
|
|
|
|
function paymentMethod(v: PaymentMethod) {
|
|
const className =
|
|
"flex items-center justify-between px-3 py-2 bg-neutral-900 rounded-xl cursor-pointer";
|
|
|
|
switch (v.name) {
|
|
case "lightning": {
|
|
return (
|
|
<div
|
|
key={v.name}
|
|
className={className}
|
|
onClick={() => {
|
|
setMethod(v);
|
|
renew(v.name);
|
|
}}
|
|
>
|
|
<div>
|
|
{v.name.toUpperCase()} ({v.currencies.join(",")})
|
|
</div>
|
|
<div className="rounded-lg p-2 bg-green-800">Pay Now</div>
|
|
</div>
|
|
);
|
|
}
|
|
case "revolut": {
|
|
const pkey = v.metadata?.["pubkey"];
|
|
if (!pkey) return <b>Missing Revolut pubkey</b>;
|
|
return (
|
|
<div key={v.name} className={className}>
|
|
<div>
|
|
{v.name.toUpperCase()} ({v.currencies.join(",")})
|
|
</div>
|
|
{state && (
|
|
<RevolutPayWidget
|
|
mode={import.meta.env.VITE_REVOLUT_MODE}
|
|
pubkey={pkey}
|
|
amount={state.template.cost_plan}
|
|
onPaid={() => {
|
|
onPaid();
|
|
}}
|
|
loadOrder={async () => {
|
|
if (!login?.api || !state) {
|
|
throw new Error("Not logged in");
|
|
}
|
|
const p = await login.api.renewVm(state.id, v.name);
|
|
return p.data.revolut!.token;
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
const loadPaymentMethods = useCallback(
|
|
async function () {
|
|
if (!login?.api || !state) return;
|
|
const p = await login?.api.getPaymentMethods();
|
|
setMethods(p);
|
|
},
|
|
[login?.api, state],
|
|
);
|
|
|
|
const renew = useCallback(
|
|
async function (m: string) {
|
|
if (!login?.api || !state) return;
|
|
const p = await login?.api.renewVm(state.id, m);
|
|
setPayment(p);
|
|
},
|
|
[login?.api, state],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (params["action"] === "renew" && login && state) {
|
|
loadPaymentMethods();
|
|
}
|
|
if (login && state) {
|
|
listPayments();
|
|
}
|
|
}, [login, state, params, renew]);
|
|
|
|
if (!state) return;
|
|
const expireDate = new Date(state.expires);
|
|
const days =
|
|
(expireDate.getTime() - new Date().getTime()) / 1000 / 24 / 60 / 60;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<Link to={"/vm"} state={state}>
|
|
< Back
|
|
</Link>
|
|
<div className="text-xl bg-neutral-900 rounded-xl px-3 py-4 flex justify-between items-center">
|
|
<div>Renewal for #{state.id}</div>
|
|
<div>
|
|
<CostLabel cost={state.template.cost_plan} />
|
|
<span className="text-sm">ex. tax</span>
|
|
</div>
|
|
</div>
|
|
{days > 0 && (
|
|
<div>
|
|
Expires: {expireDate.toDateString()} ({Math.floor(days)} days)
|
|
</div>
|
|
)}
|
|
{days < 0 && !methods && (
|
|
<div className="text-red-500 text-xl">Expired</div>
|
|
)}
|
|
{!methods && (
|
|
<div>
|
|
<AsyncButton onClick={loadPaymentMethods}>Extend Now</AsyncButton>
|
|
</div>
|
|
)}
|
|
{methods && !method && (
|
|
<>
|
|
<div className="text-xl">Payment Method:</div>
|
|
{methods.map((v) => paymentMethod(v))}
|
|
</>
|
|
)}
|
|
{payment && (
|
|
<>
|
|
<h3>Renew VPS</h3>
|
|
<VpsPayment
|
|
payment={payment}
|
|
onPaid={async () => {
|
|
setPayment(undefined);
|
|
onPaid();
|
|
}}
|
|
/>
|
|
</>
|
|
)}
|
|
<div className="text-xl">Payment History</div>
|
|
<table className="table bg-neutral-900 rounded-xl text-center">
|
|
<thead>
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Amount</th>
|
|
<th>Time</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{payments.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime())
|
|
.map(a => <tr key={a.id}>
|
|
<td className="pl-4">{new Date(a.created).toLocaleString()}</td>
|
|
<td><CostAmount cost={{ amount: (a.amount + a.tax) / (a.currency === "BTC" ? 1e11 : 100), currency: a.currency }} converted={false} /></td>
|
|
<td>{timeValue(a.time)}</td>
|
|
<td>{a.is_paid ? "Paid" : (new Date(a.expires) <= new Date() ? "Expired" : "Unpaid")}</td>
|
|
</tr>)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|