feat: show lnurl as payment method
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@ -210,7 +210,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>>(
|
||||||
|
@ -8,6 +8,9 @@ import CostLabel, { CostAmount } from "../components/cost";
|
|||||||
import { RevolutPayWidget } from "../components/revolut";
|
import { RevolutPayWidget } from "../components/revolut";
|
||||||
import { timeValue } from "../utils";
|
import { timeValue } from "../utils";
|
||||||
import { Icon } from "../components/icon";
|
import { Icon } from "../components/icon";
|
||||||
|
import { ApiUrl } from "../const";
|
||||||
|
import QrCode from "../components/qr";
|
||||||
|
import { LNURL } from "@snort/shared";
|
||||||
|
|
||||||
export function VmBillingPage() {
|
export function VmBillingPage() {
|
||||||
const location = useLocation() as { state?: VmInstance };
|
const location = useLocation() as { state?: VmInstance };
|
||||||
@ -48,7 +51,29 @@ export function VmBillingPage() {
|
|||||||
const className =
|
const className =
|
||||||
"flex items-center justify-between px-3 py-2 bg-neutral-900 rounded-xl cursor-pointer";
|
"flex items-center justify-between px-3 py-2 bg-neutral-900 rounded-xl cursor-pointer";
|
||||||
|
|
||||||
|
const nameRow = (v: PaymentMethod) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{v.name.toUpperCase()} ({v.currencies.join(",")})
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
switch (v.name) {
|
switch (v.name) {
|
||||||
|
case "lnurl": {
|
||||||
|
const addr = v.metadata?.["address"];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={v.name}
|
||||||
|
className={className}
|
||||||
|
onClick={() => {
|
||||||
|
setMethod(v);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{nameRow(v)}
|
||||||
|
<div>{addr}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
case "lightning": {
|
case "lightning": {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -59,9 +84,7 @@ export function VmBillingPage() {
|
|||||||
renew(v.name);
|
renew(v.name);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
{nameRow(v)}
|
||||||
{v.name.toUpperCase()} ({v.currencies.join(",")})
|
|
||||||
</div>
|
|
||||||
<div className="rounded-lg p-2 bg-green-800">Pay Now</div>
|
<div className="rounded-lg p-2 bg-green-800">Pay Now</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -71,9 +94,7 @@ export function VmBillingPage() {
|
|||||||
if (!pkey) return <b>Missing Revolut pubkey</b>;
|
if (!pkey) return <b>Missing Revolut pubkey</b>;
|
||||||
return (
|
return (
|
||||||
<div key={v.name} className={className}>
|
<div key={v.name} className={className}>
|
||||||
<div>
|
{nameRow(v)}
|
||||||
{v.name.toUpperCase()} ({v.currencies.join(",")})
|
|
||||||
</div>
|
|
||||||
{state && (
|
{state && (
|
||||||
<RevolutPayWidget
|
<RevolutPayWidget
|
||||||
mode={import.meta.env.VITE_REVOLUT_MODE}
|
mode={import.meta.env.VITE_REVOLUT_MODE}
|
||||||
@ -129,6 +150,16 @@ export function VmBillingPage() {
|
|||||||
const days =
|
const days =
|
||||||
(expireDate.getTime() - new Date().getTime()) / 1000 / 24 / 60 / 60;
|
(expireDate.getTime() - new Date().getTime()) / 1000 / 24 / 60 / 60;
|
||||||
|
|
||||||
|
const lud16 = `${state.id}@${new URL(ApiUrl).host}`;
|
||||||
|
// Static LNURL payment method
|
||||||
|
const lnurl = {
|
||||||
|
name: "lnurl",
|
||||||
|
currencies: ["BTC"],
|
||||||
|
metadata: {
|
||||||
|
address: lud16,
|
||||||
|
},
|
||||||
|
} as PaymentMethod;
|
||||||
|
|
||||||
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}>
|
||||||
@ -157,7 +188,23 @@ export function VmBillingPage() {
|
|||||||
{methods && !method && (
|
{methods && !method && (
|
||||||
<>
|
<>
|
||||||
<div className="text-xl">Payment Method:</div>
|
<div className="text-xl">Payment Method:</div>
|
||||||
{methods.map((v) => paymentMethod(v))}
|
{[lnurl, ...methods].map((v) => paymentMethod(v))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{method?.name === "lnurl" && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-4 rounded-xl p-3 bg-neutral-900 items-center">
|
||||||
|
<QrCode
|
||||||
|
data={`lightning:${new LNURL(lud16).lnurl}`}
|
||||||
|
width={512}
|
||||||
|
height={512}
|
||||||
|
avatar="/logo.jpg"
|
||||||
|
className="cursor-pointer rounded-xl overflow-hidden"
|
||||||
|
/>
|
||||||
|
<div className="monospace select-all break-all text-center text-sm">
|
||||||
|
{lud16}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{payment && (
|
{payment && (
|
||||||
@ -172,35 +219,69 @@ export function VmBillingPage() {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="text-xl">Payment History</div>
|
{!methods && (
|
||||||
<table className="table bg-neutral-900 rounded-xl text-center">
|
<>
|
||||||
<thead>
|
<div className="text-xl">Payment History</div>
|
||||||
<tr>
|
<table className="table bg-neutral-900 rounded-xl text-center">
|
||||||
<th>Date</th>
|
<thead>
|
||||||
<th>Amount</th>
|
<tr>
|
||||||
<th>Time</th>
|
<th>Date</th>
|
||||||
<th>Status</th>
|
<th>Amount</th>
|
||||||
<th></th>
|
<th>Time</th>
|
||||||
</tr>
|
<th>Status</th>
|
||||||
</thead>
|
<th></th>
|
||||||
<tbody>
|
</tr>
|
||||||
{payments.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime())
|
</thead>
|
||||||
.map(a => <tr key={a.id}>
|
<tbody>
|
||||||
<td className="pl-4">{new Date(a.created).toLocaleString()}</td>
|
{payments
|
||||||
<td><CostAmount cost={{ amount: (a.amount + a.tax) / (a.currency === "BTC" ? 1e11 : 100), currency: a.currency }} converted={false} /></td>
|
.sort(
|
||||||
<td>{timeValue(a.time)}</td>
|
(a, b) =>
|
||||||
<td>{a.is_paid ? "Paid" : (new Date(a.expires) <= new Date() ? "Expired" : "Unpaid")}</td>
|
new Date(b.created).getTime() -
|
||||||
<td>
|
new Date(a.created).getTime(),
|
||||||
{a.is_paid && <div title="Generate Invoice" onClick={async () => {
|
)
|
||||||
const l = await login?.api.invoiceLink(a.id);
|
.map((a) => (
|
||||||
window.open(l, "_blank");
|
<tr key={a.id}>
|
||||||
}}>
|
<td className="pl-4">
|
||||||
<Icon name="printer" />
|
{new Date(a.created).toLocaleString()}
|
||||||
</div>}
|
</td>
|
||||||
</td>
|
<td>
|
||||||
</tr>)}
|
<CostAmount
|
||||||
</tbody>
|
cost={{
|
||||||
</table>
|
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>
|
||||||
|
<td>
|
||||||
|
{a.is_paid && (
|
||||||
|
<div
|
||||||
|
title="Generate Invoice"
|
||||||
|
onClick={async () => {
|
||||||
|
const l = await login?.api.invoiceLink(a.id);
|
||||||
|
window.open(l, "_blank");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="printer" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -59,9 +59,10 @@ export default function VmPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasNoIps = (state?.ip_assignments?.length ?? 0) === 0;
|
||||||
function networkInfo() {
|
function networkInfo() {
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
if ((state.ip_assignments?.length ?? 0) === 0) {
|
if (hasNoIps) {
|
||||||
return <div className="text-sm text-red-500">No IP's assigned</div>;
|
return <div className="text-sm text-red-500">No IP's assigned</div>;
|
||||||
}
|
}
|
||||||
return <>{state.ip_assignments?.map((i) => ipRow(i, true))}</>;
|
return <>{state.ip_assignments?.map((i) => ipRow(i, true))}</>;
|
||||||
@ -86,9 +87,7 @@ export default function VmPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Link to={"/account"}>
|
<Link to={"/account"}>< Back</Link>
|
||||||
< Back
|
|
||||||
</Link>
|
|
||||||
<VpsInstanceRow vm={state} actions={true} />
|
<VpsInstanceRow vm={state} actions={true} />
|
||||||
|
|
||||||
<div className="text-xl">Network:</div>
|
<div className="text-xl">Network:</div>
|
||||||
@ -102,12 +101,14 @@ export default function VmPage() {
|
|||||||
</div>
|
</div>
|
||||||
<Icon name="pencil" onClick={() => setEditKey(true)} />
|
<Icon name="pencil" onClick={() => setEditKey(true)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-neutral-900 px-2 py-3 rounded-lg flex gap-2 items-center">
|
{!hasNoIps && (
|
||||||
<div>Login:</div>
|
<div className="bg-neutral-900 px-2 py-3 rounded-lg flex gap-2 items-center">
|
||||||
<pre className="select-all bg-neutral-800 px-3 py-1 rounded-full">
|
<div>Login:</div>
|
||||||
ssh {state.image.default_username}@{bestHost()}
|
<pre className="select-all bg-neutral-800 px-3 py-1 rounded-full">
|
||||||
</pre>
|
ssh {state.image.default_username}@{bestHost()}
|
||||||
</div>
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
|
@ -64,11 +64,16 @@ export function timeValue(n: number): string {
|
|||||||
if (n >= 3600) {
|
if (n >= 3600) {
|
||||||
const hours = Math.floor(n / 3600);
|
const hours = Math.floor(n / 3600);
|
||||||
const minutes = Math.floor((n % 3600) / 60);
|
const minutes = Math.floor((n % 3600) / 60);
|
||||||
return hours + " hr" + (hours !== 1 ? "s" : "") + (minutes > 0 ? " " + minutes + " min" + (minutes !== 1 ? "s" : "") : "");
|
return (
|
||||||
|
hours +
|
||||||
|
" hr" +
|
||||||
|
(hours !== 1 ? "s" : "") +
|
||||||
|
(minutes > 0 ? " " + minutes + " min" + (minutes !== 1 ? "s" : "") : "")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (n >= 60) {
|
if (n >= 60) {
|
||||||
const minutes = Math.floor(n / 60);
|
const minutes = Math.floor(n / 60);
|
||||||
return minutes + " min" + (minutes !== 1 ? "s" : "");
|
return minutes + " min" + (minutes !== 1 ? "s" : "");
|
||||||
}
|
}
|
||||||
return n + " sec" + (n !== 1 ? "s" : "");
|
return n + " sec" + (n !== 1 ? "s" : "");
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user