feat: taxes
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing

ref: LNVPS/api#18
This commit is contained in:
2025-03-11 15:59:32 +00:00
parent c1312d97f1
commit 26d36adbeb
9 changed files with 149 additions and 116 deletions

View File

@ -1 +1,2 @@
#VITE_API_URL="http://localhost:8000" VITE_API_URL="http://localhost:8000"
VITE_REVOLUT_MODE="sandbox"

View File

@ -20,6 +20,7 @@
"@xterm/addon-webgl": "^0.18.0", "@xterm/addon-webgl": "^0.18.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"iso-3166-1": "^2.1.1",
"marked": "^15.0.7", "marked": "^15.0.7",
"qr-code-styling": "^1.8.4", "qr-code-styling": "^1.8.4",
"react": "^18.3.1", "react": "^18.3.1",

View File

@ -24,6 +24,7 @@ export interface AccountDetail {
email?: string; email?: string;
contact_nip17: boolean; contact_nip17: boolean;
contact_email: boolean; contact_email: boolean;
country_code: string;
} }
export interface VmCostPlan { export interface VmCostPlan {
@ -141,6 +142,7 @@ export interface VmPayment {
created: string; created: string;
expires: string; expires: string;
amount: number; amount: number;
tax: number;
is_paid: boolean; is_paid: boolean;
data: { data: {
lightning?: string; lightning?: string;

View File

@ -1,10 +1,12 @@
import RevolutCheckout, { Mode } from "@revolut/checkout"; import RevolutCheckout, { Mode } from "@revolut/checkout";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { VmCostPlan } from "../api";
interface RevolutProps { interface RevolutProps {
amount: { amount: VmCostPlan | {
currency: string;
amount: number; amount: number;
currency: string;
tax?: number;
}; };
pubkey: string; pubkey: string;
loadOrder: () => Promise<string>; loadOrder: () => Promise<string>;
@ -25,12 +27,11 @@ export function RevolutPayWidget({
async function load(pubkey: string, ref: HTMLDivElement) { async function load(pubkey: string, ref: HTMLDivElement) {
const { revolutPay } = await RevolutCheckout.payments({ const { revolutPay } = await RevolutCheckout.payments({
locale: "auto", locale: "auto",
mode: (mode ?? "sandbox") as Mode, mode: (mode ?? "prod") as Mode,
publicToken: pubkey, publicToken: pubkey,
}); });
ref.innerHTML = ""; ref.innerHTML = "";
revolutPay.mount(ref, { revolutPay.mount(ref, {
sessionToken: "",
currency: amount.currency, currency: amount.currency,
totalAmount: amount.amount, totalAmount: amount.amount,
createOrder: async () => { createOrder: async () => {

View File

@ -48,7 +48,12 @@ export default function VpsPayment({
avatar="/logo.jpg" avatar="/logo.jpg"
className="cursor-pointer rounded-xl overflow-hidden" className="cursor-pointer rounded-xl overflow-hidden"
/> />
{(payment.amount / 1000).toLocaleString()} sats <div className="flex flex-col items-center">
<div>{((payment.amount + payment.tax) / 1000).toLocaleString()} sats</div>
{payment.tax > 0 && <div className="text-xs">
including {(payment.tax / 1000).toLocaleString()} sats tax
</div>}
</div>
<div className="monospace select-all break-all text-center text-sm"> <div className="monospace select-all break-all text-center text-sm">
{invoice} {invoice}
</div> </div>

View File

@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
import { AccountDetail } from "../api"; import { AccountDetail } from "../api";
import { AsyncButton } from "../components/button"; import { AsyncButton } from "../components/button";
import { Icon } from "../components/icon"; import { Icon } from "../components/icon";
import { default as iso } from "iso-3166-1";
export function AccountSettings() { export function AccountSettings() {
const login = useLogin(); const login = useLogin();
@ -13,9 +14,23 @@ export function AccountSettings() {
login?.api.getAccount().then(setAcc); login?.api.getAccount().then(setAcc);
}, [login]); }, [login]);
function notifications() { if (!acc) return;
return ( return <div className="flex flex-col gap-4">
<> <h3>
Account Settings
</h3>
<div className="flex gap-2 items-center">
<h4>Country</h4>
<select value={acc?.country_code}
onChange={(e) =>
setAcc((s) => (s ? { ...s, country_code: e.target.value } : undefined))
}
>
{iso.all().map(c => <option value={c.alpha3}>{c.country}</option>)}
</select>
</div>
<h3>Notification Settings</h3> <h3>Notification Settings</h3>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<input <input
@ -67,9 +82,5 @@ export function AccountSettings() {
Save Save
</AsyncButton> </AsyncButton>
</div> </div>
</> </div>
);
}
return <>{notifications()}</>;
} }

View File

@ -34,8 +34,8 @@ export default function HomePage() {
{offers?.custom_template && ( {offers?.custom_template && (
<VpsCustomOrder templates={offers.custom_template} /> <VpsCustomOrder templates={offers.custom_template} />
)} )}
<small className="text-neutral-400"> <small className="text-neutral-400 text-center">
All VPS come with 1x IPv4 and 1x IPv6 address and unmetered traffic All VPS come with 1x IPv4 and 1x IPv6 address and unmetered traffic, all prices are excluding taxes.
</small> </small>
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="text-center"> <div className="text-center">

View File

@ -67,6 +67,7 @@ export function VmBillingPage() {
</div> </div>
{state && ( {state && (
<RevolutPayWidget <RevolutPayWidget
mode={import.meta.env.VITE_REVOLUT_MODE}
pubkey={pkey} pubkey={pkey}
amount={state.template.cost_plan} amount={state.template.cost_plan}
onPaid={() => { onPaid={() => {
@ -122,7 +123,10 @@ export function VmBillingPage() {
</Link> </Link>
<div className="text-xl bg-neutral-900 rounded-xl px-3 py-4 flex justify-between items-center"> <div className="text-xl bg-neutral-900 rounded-xl px-3 py-4 flex justify-between items-center">
<div>Renewal for #{state.id}</div> <div>Renewal for #{state.id}</div>
<div>
<CostLabel cost={state.template.cost_plan} /> <CostLabel cost={state.template.cost_plan} />
<span className="text-sm">ex. tax</span>
</div>
</div> </div>
{days > 0 && ( {days > 0 && (
<div> <div>

View File

@ -2451,6 +2451,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"iso-3166-1@npm:^2.1.1":
version: 2.1.1
resolution: "iso-3166-1@npm:2.1.1"
checksum: 10c0/65daf0283d22b2848d733a50ba6116a01df3e4d77970bb31d0df0696b4b386bdad0946dea53c9bb1e748e5e5e74608c07b6f4fe044908a5cffbf46f315385400
languageName: node
linkType: hard
"isomorphic-ws@npm:^5.0.0": "isomorphic-ws@npm:^5.0.0":
version: 5.0.0 version: 5.0.0
resolution: "isomorphic-ws@npm:5.0.0" resolution: "isomorphic-ws@npm:5.0.0"
@ -2618,6 +2625,7 @@ __metadata:
eslint-plugin-react-hooks: "npm:^5.1.0-rc.0" eslint-plugin-react-hooks: "npm:^5.1.0-rc.0"
eslint-plugin-react-refresh: "npm:^0.4.9" eslint-plugin-react-refresh: "npm:^0.4.9"
globals: "npm:^15.9.0" globals: "npm:^15.9.0"
iso-3166-1: "npm:^2.1.1"
marked: "npm:^15.0.7" marked: "npm:^15.0.7"
postcss: "npm:^8.4.41" postcss: "npm:^8.4.41"
prettier: "npm:^3.3.3" prettier: "npm:^3.3.3"