ref: LNVPS/api#24
This commit is contained in:
@ -10,6 +10,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@revolut/checkout": "^1.1.20",
|
||||
"@scure/base": "^1.2.1",
|
||||
"@snort/shared": "^1.0.17",
|
||||
"@snort/system": "^1.6.1",
|
||||
|
24
src/api.ts
24
src/api.ts
@ -138,11 +138,16 @@ export interface UserSshKey {
|
||||
|
||||
export interface VmPayment {
|
||||
id: string;
|
||||
invoice: string;
|
||||
created: string;
|
||||
expires: string;
|
||||
amount: number;
|
||||
is_paid: boolean;
|
||||
data: {
|
||||
lightning?: string;
|
||||
revolut?: {
|
||||
token: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface PatchVm {
|
||||
@ -161,6 +166,12 @@ export interface TimeSeriesData {
|
||||
disk_read: number;
|
||||
}
|
||||
|
||||
export interface PaymentMethod {
|
||||
name: string;
|
||||
currencies: Array<string>;
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
export class LNVpsApi {
|
||||
constructor(
|
||||
readonly url: string,
|
||||
@ -295,9 +306,9 @@ export class LNVpsApi {
|
||||
return data;
|
||||
}
|
||||
|
||||
async renewVm(vm_id: number) {
|
||||
async renewVm(vm_id: number, method: string) {
|
||||
const { data } = await this.#handleResponse<ApiResponse<VmPayment>>(
|
||||
await this.#req(`/api/v1/vm/${vm_id}/renew`, "GET"),
|
||||
await this.#req(`/api/v1/vm/${vm_id}/renew?method=${method}`, "GET"),
|
||||
);
|
||||
return data;
|
||||
}
|
||||
@ -309,6 +320,13 @@ export class LNVpsApi {
|
||||
return data;
|
||||
}
|
||||
|
||||
async getPaymentMethods() {
|
||||
const { data } = await this.#handleResponse<
|
||||
ApiResponse<Array<PaymentMethod>>
|
||||
>(await this.#req("/api/v1/payment/methods", "GET"));
|
||||
return data;
|
||||
}
|
||||
|
||||
async connect_terminal(id: number) {
|
||||
const u = `${this.url}/api/v1/console/${id}`;
|
||||
const auth = await this.#auth_event(u, "GET");
|
||||
|
@ -1,7 +1,16 @@
|
||||
interface Price { currency: string, amount: number }
|
||||
type Cost = Price & { interval_type?: string, other_price?: Array<Price> }
|
||||
interface Price {
|
||||
currency: string;
|
||||
amount: number;
|
||||
}
|
||||
type Cost = Price & { interval_type?: string; other_price?: Array<Price> };
|
||||
|
||||
export default function CostLabel({ cost, converted }: { cost: Cost, converted?: boolean }) {
|
||||
export default function CostLabel({
|
||||
cost,
|
||||
converted,
|
||||
}: {
|
||||
cost: Cost;
|
||||
converted?: boolean;
|
||||
}) {
|
||||
function intervalName(n: string) {
|
||||
switch (n) {
|
||||
case "day":
|
||||
@ -16,13 +25,17 @@ export default function CostLabel({ cost, converted }: { cost: Cost, converted?:
|
||||
return (
|
||||
<div>
|
||||
{converted && "~"}
|
||||
{cost.currency !== "BTC" ? cost.amount.toFixed(2) : Math.floor(cost.amount * 1e8).toLocaleString()}
|
||||
{" "}
|
||||
{cost.currency !== "BTC"
|
||||
? cost.amount.toFixed(2)
|
||||
: Math.floor(cost.amount * 1e8).toLocaleString()}{" "}
|
||||
{cost.currency === "BTC" ? "sats" : cost.currency}
|
||||
{cost.interval_type && <>/{intervalName(cost.interval_type)}</>}
|
||||
{cost.other_price && cost.other_price.map(a => <div key={a.currency} className="text-xs">
|
||||
{cost.other_price &&
|
||||
cost.other_price.map((a) => (
|
||||
<div key={a.currency} className="text-xs">
|
||||
<CostLabel cost={a} converted={true} />
|
||||
</div>)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
64
src/components/revolut.tsx
Normal file
64
src/components/revolut.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import RevolutCheckout, { Mode } from "@revolut/checkout";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
interface RevolutProps {
|
||||
amount: {
|
||||
currency: string;
|
||||
amount: number;
|
||||
};
|
||||
pubkey: string;
|
||||
loadOrder: () => Promise<string>;
|
||||
onPaid: () => void;
|
||||
onCancel?: () => void;
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
export function RevolutPayWidget({
|
||||
pubkey,
|
||||
loadOrder,
|
||||
amount,
|
||||
onPaid,
|
||||
onCancel,
|
||||
mode,
|
||||
}: RevolutProps) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
async function load(pubkey: string, ref: HTMLDivElement) {
|
||||
const { revolutPay } = await RevolutCheckout.payments({
|
||||
locale: "auto",
|
||||
mode: (mode ?? "sandbox") as Mode,
|
||||
publicToken: pubkey,
|
||||
});
|
||||
ref.innerHTML = "";
|
||||
revolutPay.mount(ref, {
|
||||
sessionToken: "",
|
||||
currency: amount.currency,
|
||||
totalAmount: amount.amount,
|
||||
createOrder: async () => {
|
||||
const id = await loadOrder();
|
||||
return {
|
||||
publicId: id,
|
||||
};
|
||||
},
|
||||
buttonStyle: {
|
||||
cashback: false,
|
||||
},
|
||||
});
|
||||
revolutPay.on("payment", (payload) => {
|
||||
console.debug(payload);
|
||||
if (payload.type === "success") {
|
||||
onPaid();
|
||||
}
|
||||
if (payload.type === "cancel") {
|
||||
onCancel?.();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
load(pubkey, ref.current);
|
||||
}
|
||||
}, [pubkey, ref]);
|
||||
|
||||
return <div ref={ref}></div>;
|
||||
}
|
@ -11,7 +11,8 @@ export default function VpsPayment({
|
||||
onPaid?: () => void;
|
||||
}) {
|
||||
const login = useLogin();
|
||||
const ln = `lightning:${payment.invoice}`;
|
||||
const invoice = payment.data.lightning;
|
||||
const ln = `lightning:${invoice}`;
|
||||
|
||||
async function checkPayment(api: LNVpsApi) {
|
||||
try {
|
||||
@ -49,7 +50,7 @@ export default function VpsPayment({
|
||||
/>
|
||||
{(payment.amount / 1000).toLocaleString()} sats
|
||||
<div className="monospace select-all break-all text-center text-sm">
|
||||
{payment.invoice}
|
||||
{invoice}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,16 +1,19 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { VmInstance, VmPayment } from "../api";
|
||||
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 { RevolutPayWidget } from "../components/revolut";
|
||||
|
||||
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 [state, setState] = useState<VmInstance | undefined>(location?.state);
|
||||
|
||||
@ -18,13 +21,85 @@ export function VmBillingPage() {
|
||||
if (!state) return;
|
||||
const newState = await login?.api.getVm(state.id);
|
||||
setState(newState);
|
||||
setMethod(undefined);
|
||||
setMethods(undefined);
|
||||
return newState;
|
||||
}
|
||||
|
||||
const renew = useCallback(
|
||||
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
|
||||
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.renewVm(state.id);
|
||||
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],
|
||||
@ -32,7 +107,7 @@ export function VmBillingPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (params["action"] === "renew" && login && state) {
|
||||
renew();
|
||||
loadPaymentMethods();
|
||||
}
|
||||
}, [login, state, params, renew]);
|
||||
|
||||
@ -54,14 +129,20 @@ export function VmBillingPage() {
|
||||
Expires: {expireDate.toDateString()} ({Math.floor(days)} days)
|
||||
</div>
|
||||
)}
|
||||
{days < 0 && params["action"] !== "renew" && (
|
||||
{days < 0 && !methods && (
|
||||
<div className="text-red-500 text-xl">Expired</div>
|
||||
)}
|
||||
{!payment && (
|
||||
{!methods && (
|
||||
<div>
|
||||
<AsyncButton onClick={renew}>Extend Now</AsyncButton>
|
||||
<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>
|
||||
@ -69,11 +150,7 @@ export function VmBillingPage() {
|
||||
payment={payment}
|
||||
onPaid={async () => {
|
||||
setPayment(undefined);
|
||||
if (!login?.api || !state) return;
|
||||
const s = await reloadVmState();
|
||||
if (params["action"] === "renew") {
|
||||
navigate("/vm", { state: s });
|
||||
}
|
||||
onPaid();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
@ -620,6 +620,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@revolut/checkout@npm:^1.1.20":
|
||||
version: 1.1.20
|
||||
resolution: "@revolut/checkout@npm:1.1.20"
|
||||
checksum: 10c0/8c1053a434ff759a4101f757e08a341331993e4be210fdd6c5def7e452ead9710dba300efe30fd8570a6686bd445ecd15716f5a5c93996a6d60713946e6317a0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@npm:4.20.0":
|
||||
version: 4.20.0
|
||||
resolution: "@rollup/rollup-android-arm-eabi@npm:4.20.0"
|
||||
@ -2593,6 +2600,7 @@ __metadata:
|
||||
resolution: "lnvps_web@workspace:."
|
||||
dependencies:
|
||||
"@eslint/js": "npm:^9.8.0"
|
||||
"@revolut/checkout": "npm:^1.1.20"
|
||||
"@scure/base": "npm:^1.2.1"
|
||||
"@snort/shared": "npm:^1.0.17"
|
||||
"@snort/system": "npm:^1.6.1"
|
||||
|
Reference in New Issue
Block a user