From 9a04548627c1a2a7cadbdf0a66ff524ac396eda8 Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 1 May 2025 15:32:00 +0100 Subject: [PATCH] feat: payment history --- src/api.ts | 11 ++++++++++- src/pages/vm-billing.tsx | 34 +++++++++++++++++++++++++++++++++- src/pages/vm.tsx | 5 ++++- src/utils.ts | 21 +++++++++++++++++++++ 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/api.ts b/src/api.ts index 89da961..4fc8d7d 100644 --- a/src/api.ts +++ b/src/api.ts @@ -149,8 +149,10 @@ export interface VmPayment { created: string; expires: string; amount: number; + currency: string; tax: number; is_paid: boolean; + time: number; data: { lightning?: string; revolut?: { @@ -207,7 +209,7 @@ export class LNVpsApi { constructor( readonly url: string, readonly publisher: EventPublisher | undefined, - ) {} + ) { } async getAccount() { const { data } = await this.#handleResponse>( @@ -358,6 +360,13 @@ export class LNVpsApi { return data; } + async listPayments(id: number) { + const { data } = await this.#handleResponse>>( + await this.#req(`/api/v1/vm/${id}/payments`, "GET"), + ); + return data; + } + async getPaymentMethods() { const { data } = await this.#handleResponse< ApiResponse> diff --git a/src/pages/vm-billing.tsx b/src/pages/vm-billing.tsx index 0cab88f..a7cbcba 100644 --- a/src/pages/vm-billing.tsx +++ b/src/pages/vm-billing.tsx @@ -4,8 +4,9 @@ import { PaymentMethod, VmInstance, VmPayment } from "../api"; import VpsPayment from "../components/vps-payment"; import useLogin from "../hooks/login"; import { AsyncButton } from "../components/button"; -import CostLabel from "../components/cost"; +import CostLabel, { CostAmount } from "../components/cost"; import { RevolutPayWidget } from "../components/revolut"; +import { timeValue } from "../utils"; export function VmBillingPage() { const location = useLocation() as { state?: VmInstance }; @@ -15,8 +16,15 @@ export function VmBillingPage() { const [methods, setMethods] = useState>(); const [method, setMethod] = useState(); const [payment, setPayment] = useState(); + const [payments, setPayments] = useState>([]); const [state, setState] = useState(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); @@ -110,12 +118,16 @@ export function VmBillingPage() { 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 (
@@ -159,6 +171,26 @@ export function VmBillingPage() { /> )} +
Payment History
+ + + + + + + + + + + {payments.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime()) + .map(a => + + + + + )} + +
DateAmountTimeStatus
{new Date(a.created).toLocaleString()}{timeValue(a.time)}{a.is_paid ? "Paid" : (new Date(a.expires) <= new Date() ? "Expired" : "Unpaid")}
); } diff --git a/src/pages/vm.tsx b/src/pages/vm.tsx index 9a39f47..c590d54 100644 --- a/src/pages/vm.tsx +++ b/src/pages/vm.tsx @@ -1,6 +1,6 @@ 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 VpsInstanceRow from "../components/vps-instance"; import useLogin from "../hooks/login"; @@ -78,6 +78,9 @@ export default function VmPage() { return (
+ + < Back +
Network:
diff --git a/src/utils.ts b/src/utils.ts index 8b3a435..eecd6b9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -51,3 +51,24 @@ export function toEui64(prefix: string, mac: string) { base16.encode(macExtended.subarray(6, 8)) ).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" : ""); +} \ No newline at end of file