feat: payment history
This commit is contained in:
11
src/api.ts
11
src/api.ts
@ -149,8 +149,10 @@ export interface VmPayment {
|
|||||||
created: string;
|
created: string;
|
||||||
expires: string;
|
expires: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
|
currency: string;
|
||||||
tax: number;
|
tax: number;
|
||||||
is_paid: boolean;
|
is_paid: boolean;
|
||||||
|
time: number;
|
||||||
data: {
|
data: {
|
||||||
lightning?: string;
|
lightning?: string;
|
||||||
revolut?: {
|
revolut?: {
|
||||||
@ -207,7 +209,7 @@ export class LNVpsApi {
|
|||||||
constructor(
|
constructor(
|
||||||
readonly url: string,
|
readonly url: string,
|
||||||
readonly publisher: EventPublisher | undefined,
|
readonly publisher: EventPublisher | undefined,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
async getAccount() {
|
async getAccount() {
|
||||||
const { data } = await this.#handleResponse<ApiResponse<AccountDetail>>(
|
const { data } = await this.#handleResponse<ApiResponse<AccountDetail>>(
|
||||||
@ -358,6 +360,13 @@ export class LNVpsApi {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listPayments(id: number) {
|
||||||
|
const { data } = await this.#handleResponse<ApiResponse<Array<VmPayment>>>(
|
||||||
|
await this.#req(`/api/v1/vm/${id}/payments`, "GET"),
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
async getPaymentMethods() {
|
async getPaymentMethods() {
|
||||||
const { data } = await this.#handleResponse<
|
const { data } = await this.#handleResponse<
|
||||||
ApiResponse<Array<PaymentMethod>>
|
ApiResponse<Array<PaymentMethod>>
|
||||||
|
@ -4,8 +4,9 @@ import { PaymentMethod, VmInstance, VmPayment } from "../api";
|
|||||||
import VpsPayment from "../components/vps-payment";
|
import VpsPayment from "../components/vps-payment";
|
||||||
import useLogin from "../hooks/login";
|
import useLogin from "../hooks/login";
|
||||||
import { AsyncButton } from "../components/button";
|
import { AsyncButton } from "../components/button";
|
||||||
import CostLabel from "../components/cost";
|
import CostLabel, { CostAmount } from "../components/cost";
|
||||||
import { RevolutPayWidget } from "../components/revolut";
|
import { RevolutPayWidget } from "../components/revolut";
|
||||||
|
import { timeValue } from "../utils";
|
||||||
|
|
||||||
export function VmBillingPage() {
|
export function VmBillingPage() {
|
||||||
const location = useLocation() as { state?: VmInstance };
|
const location = useLocation() as { state?: VmInstance };
|
||||||
@ -15,8 +16,15 @@ export function VmBillingPage() {
|
|||||||
const [methods, setMethods] = useState<Array<PaymentMethod>>();
|
const [methods, setMethods] = useState<Array<PaymentMethod>>();
|
||||||
const [method, setMethod] = useState<PaymentMethod>();
|
const [method, setMethod] = useState<PaymentMethod>();
|
||||||
const [payment, setPayment] = useState<VmPayment>();
|
const [payment, setPayment] = useState<VmPayment>();
|
||||||
|
const [payments, setPayments] = useState<Array<VmPayment>>([]);
|
||||||
const [state, setState] = useState<VmInstance | undefined>(location?.state);
|
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() {
|
async function reloadVmState() {
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
const newState = await login?.api.getVm(state.id);
|
const newState = await login?.api.getVm(state.id);
|
||||||
@ -110,12 +118,16 @@ export function VmBillingPage() {
|
|||||||
if (params["action"] === "renew" && login && state) {
|
if (params["action"] === "renew" && login && state) {
|
||||||
loadPaymentMethods();
|
loadPaymentMethods();
|
||||||
}
|
}
|
||||||
|
if (login && state) {
|
||||||
|
listPayments();
|
||||||
|
}
|
||||||
}, [login, state, params, renew]);
|
}, [login, state, params, renew]);
|
||||||
|
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
const expireDate = new Date(state.expires);
|
const expireDate = new Date(state.expires);
|
||||||
const days =
|
const days =
|
||||||
(expireDate.getTime() - new Date().getTime()) / 1000 / 24 / 60 / 60;
|
(expireDate.getTime() - new Date().getTime()) / 1000 / 24 / 60 / 60;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Link to={"/vm"} state={state}>
|
<Link to={"/vm"} state={state}>
|
||||||
@ -159,6 +171,26 @@ export function VmBillingPage() {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import "@xterm/xterm/css/xterm.css";
|
import "@xterm/xterm/css/xterm.css";
|
||||||
|
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { VmInstance, VmIpAssignment } from "../api";
|
import { VmInstance, VmIpAssignment } from "../api";
|
||||||
import VpsInstanceRow from "../components/vps-instance";
|
import VpsInstanceRow from "../components/vps-instance";
|
||||||
import useLogin from "../hooks/login";
|
import useLogin from "../hooks/login";
|
||||||
@ -78,6 +78,9 @@ export default function VmPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
|
<Link to={"/account"}>
|
||||||
|
< Back
|
||||||
|
</Link>
|
||||||
<VpsInstanceRow vm={state} actions={true} />
|
<VpsInstanceRow vm={state} actions={true} />
|
||||||
|
|
||||||
<div className="text-xl">Network:</div>
|
<div className="text-xl">Network:</div>
|
||||||
|
21
src/utils.ts
21
src/utils.ts
@ -51,3 +51,24 @@ export function toEui64(prefix: string, mac: string) {
|
|||||||
base16.encode(macExtended.subarray(6, 8))
|
base16.encode(macExtended.subarray(6, 8))
|
||||||
).toLowerCase();
|
).toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function timeValue(n: number): string {
|
||||||
|
if (!Number.isFinite(n) || n < 0) {
|
||||||
|
return "Invalid input";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n >= 86400) {
|
||||||
|
const days = Math.floor(n / 86400);
|
||||||
|
return days.toLocaleString() + " day" + (days !== 1 ? "s" : "");
|
||||||
|
}
|
||||||
|
if (n >= 3600) {
|
||||||
|
const hours = Math.floor(n / 3600);
|
||||||
|
const minutes = Math.floor((n % 3600) / 60);
|
||||||
|
return hours + " hr" + (hours !== 1 ? "s" : "") + (minutes > 0 ? " " + minutes + " min" + (minutes !== 1 ? "s" : "") : "");
|
||||||
|
}
|
||||||
|
if (n >= 60) {
|
||||||
|
const minutes = Math.floor(n / 60);
|
||||||
|
return minutes + " min" + (minutes !== 1 ? "s" : "");
|
||||||
|
}
|
||||||
|
return n + " sec" + (n !== 1 ? "s" : "");
|
||||||
|
}
|
Reference in New Issue
Block a user