@ -1 +1,2 @@
|
|||||||
#VITE_API_URL="http://localhost:8000"
|
VITE_API_URL="http://localhost:8000"
|
||||||
|
VITE_REVOLUT_MODE="sandbox"
|
@ -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",
|
||||||
|
@ -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;
|
||||||
|
@ -1,64 +1,65 @@
|
|||||||
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;
|
};
|
||||||
loadOrder: () => Promise<string>;
|
pubkey: string;
|
||||||
onPaid: () => void;
|
loadOrder: () => Promise<string>;
|
||||||
onCancel?: () => void;
|
onPaid: () => void;
|
||||||
mode?: string;
|
onCancel?: () => void;
|
||||||
|
mode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RevolutPayWidget({
|
export function RevolutPayWidget({
|
||||||
pubkey,
|
pubkey,
|
||||||
loadOrder,
|
loadOrder,
|
||||||
amount,
|
amount,
|
||||||
onPaid,
|
onPaid,
|
||||||
onCancel,
|
onCancel,
|
||||||
mode,
|
mode,
|
||||||
}: RevolutProps) {
|
}: RevolutProps) {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
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 () => {
|
const id = await loadOrder();
|
||||||
const id = await loadOrder();
|
return {
|
||||||
return {
|
publicId: id,
|
||||||
publicId: id,
|
};
|
||||||
};
|
},
|
||||||
},
|
buttonStyle: {
|
||||||
buttonStyle: {
|
cashback: false,
|
||||||
cashback: false,
|
},
|
||||||
},
|
});
|
||||||
});
|
revolutPay.on("payment", (payload) => {
|
||||||
revolutPay.on("payment", (payload) => {
|
console.debug(payload);
|
||||||
console.debug(payload);
|
if (payload.type === "success") {
|
||||||
if (payload.type === "success") {
|
onPaid();
|
||||||
onPaid();
|
}
|
||||||
}
|
if (payload.type === "cancel") {
|
||||||
if (payload.type === "cancel") {
|
onCancel?.();
|
||||||
onCancel?.();
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (ref.current) {
|
|
||||||
load(pubkey, ref.current);
|
|
||||||
}
|
}
|
||||||
}, [pubkey, ref]);
|
|
||||||
|
|
||||||
return <div ref={ref}></div>;
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
load(pubkey, ref.current);
|
||||||
|
}
|
||||||
|
}, [pubkey, ref]);
|
||||||
|
|
||||||
|
return <div ref={ref}></div>;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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,63 +14,73 @@ 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>
|
||||||
<h3>Notification Settings</h3>
|
Account Settings
|
||||||
<div className="flex gap-2 items-center">
|
</h3>
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={acc?.contact_email ?? false}
|
|
||||||
onChange={(e) => {
|
|
||||||
setAcc((s) =>
|
|
||||||
s ? { ...s, contact_email: e.target.checked } : undefined,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
Email
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={acc?.contact_nip17 ?? false}
|
|
||||||
onChange={(e) => {
|
|
||||||
setAcc((s) =>
|
|
||||||
s ? { ...s, contact_nip17: e.target.checked } : undefined,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
Nostr DM
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<h4>Email</h4>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
disabled={!editEmail}
|
|
||||||
value={acc?.email}
|
|
||||||
onChange={(e) =>
|
|
||||||
setAcc((s) => (s ? { ...s, email: e.target.value } : undefined))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{!editEmail && (
|
|
||||||
<Icon name="pencil" onClick={() => setEditEmail(true)} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<AsyncButton
|
|
||||||
onClick={async () => {
|
|
||||||
if (login?.api && acc) {
|
|
||||||
await login.api.updateAccount(acc);
|
|
||||||
const newAcc = await login.api.getAccount();
|
|
||||||
setAcc(newAcc);
|
|
||||||
setEditEmail(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</AsyncButton>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{notifications()}</>;
|
<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>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={acc?.contact_email ?? false}
|
||||||
|
onChange={(e) => {
|
||||||
|
setAcc((s) =>
|
||||||
|
s ? { ...s, contact_email: e.target.checked } : undefined,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Email
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={acc?.contact_nip17 ?? false}
|
||||||
|
onChange={(e) => {
|
||||||
|
setAcc((s) =>
|
||||||
|
s ? { ...s, contact_nip17: e.target.checked } : undefined,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Nostr DM
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<h4>Email</h4>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
disabled={!editEmail}
|
||||||
|
value={acc?.email}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAcc((s) => (s ? { ...s, email: e.target.value } : undefined))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{!editEmail && (
|
||||||
|
<Icon name="pencil" onClick={() => setEditEmail(true)} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<AsyncButton
|
||||||
|
onClick={async () => {
|
||||||
|
if (login?.api && acc) {
|
||||||
|
await login.api.updateAccount(acc);
|
||||||
|
const newAcc = await login.api.getAccount();
|
||||||
|
setAcc(newAcc);
|
||||||
|
setEditEmail(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</AsyncButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
<CostLabel cost={state.template.cost_plan} />
|
<div>
|
||||||
|
<CostLabel cost={state.template.cost_plan} />
|
||||||
|
<span className="text-sm">ex. tax</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{days > 0 && (
|
{days > 0 && (
|
||||||
<div>
|
<div>
|
||||||
|
@ -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"
|
||||||
|
Reference in New Issue
Block a user