feat: payment history

This commit is contained in:
2025-05-01 15:32:00 +01:00
parent 93de2704cc
commit 9a04548627
4 changed files with 68 additions and 3 deletions

View File

@ -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>>

View File

@ -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>
); );
} }

View File

@ -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"}>
&lt; Back
</Link>
<VpsInstanceRow vm={state} actions={true} /> <VpsInstanceRow vm={state} actions={true} />
<div className="text-xl">Network:</div> <div className="text-xl">Network:</div>

View File

@ -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" : "");
}