feat: wallet connection flow

This commit is contained in:
2024-01-03 16:46:18 +00:00
parent 0cc0a47501
commit 4d9226b3b6
8 changed files with 253 additions and 64 deletions

View File

@ -191,8 +191,8 @@ export default function WalletPage(props: { showHistory: boolean }) {
defaultMessage="<big>{amount}</big> <small>sats</small>"
id="E5ZIPD"
values={{
big: c => <span className="text-3xl font-bold">{c}</span>,
small: c => <span className="text-secondary">{c}</span>,
big: c => <span className="text-5xl font-bold">{c}</span>,
small: c => <span className="text-secondary text-sm">{c}</span>,
amount: <FormattedNumber value={balance ?? 0} />,
}}
/>

View File

@ -5,7 +5,6 @@ import Preferences from "@/Pages/settings/Preferences";
import Notifications from "@/Pages/settings/Notifications";
import RelayInfo from "@/Pages/settings/RelayInfo";
import AccountsPage from "@/Pages/settings/Accounts";
import { WalletSettingsRoutes } from "@/Pages/settings/WalletSettings";
import { ManageHandleRoutes } from "@/Pages/settings/handle";
import ExportKeys from "@/Pages/settings/Keys";
import ModerationSettings from "@/Pages/settings/Moderation";
@ -13,6 +12,7 @@ import { CacheSettings } from "@/Pages/settings/Cache";
import { ReferralsPage } from "@/Pages/settings/Referrals";
import { Outlet } from "react-router-dom";
import { ToolsPage, ToolsPages } from "./tools";
import { WalletSettingsRoutes } from "./wallet";
const SettingsPage = () => {
return (

View File

@ -1,14 +0,0 @@
.wallet-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
text-align: center;
grid-gap: 10px;
}
.wallet-grid > div {
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
}

View File

@ -1,64 +1,71 @@
import "./WalletSettings.css";
import LndLogo from "@/lnd-logo.png";
import { ReactNode } from "react";
import { FormattedMessage } from "react-intl";
import { RouteObject, useNavigate } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import BlueWallet from "@/Icons/BlueWallet";
import ConnectLNC from "@/Pages/settings/wallet/LNC";
import ConnectLNDHub from "@/Pages/settings/wallet/LNDHub";
import ConnectNostrWallet from "@/Pages/settings/wallet/NWC";
import ConnectCashu from "@/Pages/settings/wallet/Cashu";
import NostrIcon from "@/Icons/Nostrich";
import WalletPage from "../WalletPage";
import CashuIcon from "@/Icons/Cashu";
import AlbyIcon from "@/Icons/Alby";
import Icon from "@/Icons/Icon";
import { getAlbyOAuth } from "./wallet/Alby";
const WalletRow = (props: { logo: ReactNode, name: ReactNode, url: string, desc?: ReactNode }) => {
const navigate = useNavigate();
return <div className="flex items-center gap-4 px-4 py-2 bg-[--gray-superdark] rounded-xl hover:bg-[--gray-ultradark]" onClick={() => {
if (props.url.startsWith("http")) {
window.location.href = props.url;
} else {
navigate(props.url);
}
}}>
<div className="rounded-xl aspect-square h-[4rem] bg-[--gray-dark] p-3 flex items-center justify-center">
{props.logo}
</div>
<div className="flex flex-col gap-1 grow justify-center">
<div className="text-xl font-bold">{props.name}</div>
<div className="text-sm text-secondary">{props.desc}</div>
</div>
<Icon name="arrowFront" />
</div>
}
const WalletSettings = () => {
const navigate = useNavigate();
const alby = getAlbyOAuth();
return (
<>
<WalletPage showHistory={false} />
<h3>
<FormattedMessage defaultMessage="Connect Wallet" id="cg1VJ2" />
</h3>
<div className="wallet-grid">
<div onClick={() => navigate("/settings/wallet/lnc")}>
<img src={LndLogo} width={100} />
<h3>LND with LNC</h3>
</div>
<div onClick={() => navigate("/settings/wallet/lndhub")}>
<BlueWallet width={100} height={100} />
<h3>LNDHub</h3>
</div>
<div onClick={() => navigate("/settings/wallet/nwc")}>
<NostrIcon width={100} height={100} />
<h3>Nostr Wallet Connect</h3>
</div>
<div className="flex flex-col gap-3 cursor-pointer">
<WalletRow
logo={<NostrIcon width={64} height={64} />}
name="Nostr Wallet Connect"
url="/settings/wallet/nwc"
desc={<FormattedMessage defaultMessage="Native nostr wallet connection" id="cG/bKQ" />} />
<WalletRow
logo={<img src={LndLogo} />}
name="LND via LNC"
url="/settings/wallet/lnc"
desc={<FormattedMessage defaultMessage="Connect to your own LND node with Lightning Node Connect" id="aSGz4J" />} />
<WalletRow
logo={<BlueWallet width={64} height={64} />}
name="LNDHub"
url="/settings/wallet/lndhub"
desc={<FormattedMessage defaultMessage="Generic LNDHub wallet (BTCPayServer / Alby / LNBits)" id="0MndVW" />} />
<WalletRow
logo={<CashuIcon size={64} />}
name="Cashu"
url="/settings/wallet/cashu"
desc={<FormattedMessage defaultMessage="Cashu mint wallet" id="3natuV" />} />
<WalletRow
logo={<AlbyIcon size={64} />}
name="Alby"
url={alby.authUrl}
desc={<FormattedMessage defaultMessage="Alby wallet connection" id="XPB8VV" />} />
</div>
</>
);
};
export default WalletSettings;
export const WalletSettingsRoutes = [
{
path: "/settings/wallet",
element: <WalletSettings />,
},
{
path: "/settings/wallet/lnc",
element: <ConnectLNC />,
},
{
path: "/settings/wallet/lndhub",
element: <ConnectLNDHub />,
},
{
path: "/settings/wallet/nwc",
element: <ConnectNostrWallet />,
},
{
path: "/settings/wallet/cashu",
element: <ConnectCashu />,
},
] as Array<RouteObject>;
export default WalletSettings;

View File

@ -0,0 +1,98 @@
import PageSpinner from "@/Element/PageSpinner";
import { sha256 } from "@noble/hashes/sha256";
import { randomBytes } from "@noble/hashes/utils";
import { base64, base64urlnopad, hex } from "@scure/base";
import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
export default function AlbyOAuth() {
const location = useLocation();
const alby = getAlbyOAuth();
const [error, setError] = useState("");
async function setupWallet(token: string) {
const auth = await alby.getToken(token);
console.debug(auth);
}
useEffect(() => {
if (location.search) {
const params = new URLSearchParams(location.search);
const token = params.get("code");
if (token) {
setupWallet(token).catch(e => {
setError((e as Error).message);
});
}
}
}, [location]);
if (!location.search) return;
return <>
<h1>Alby Wallet Connection</h1>
{!error && <PageSpinner />}
{error && <b className="warning">{error}</b>}
</>
}
export function getAlbyOAuth() {
const clientId = "35EQp6crss";
const clientSecret = "DTUPIqOjsjwxZXcJwF5C"
const redirectUrl = `${window.location.protocol}//${window.location.host}/settings/wallet/alby`;
const scopes = [
"invoices:create",
"invoices:read",
"transactions:read",
"balance:read",
"payments:send"
];
const ec = new TextEncoder();
const code_verifier = hex.encode(randomBytes(64));
window.sessionStorage.setItem("alby-code", code_verifier);
const params = new URLSearchParams();
params.set("client_id", clientId);
params.set("response_type", "code");
params.set("code_challenge", base64urlnopad.encode(sha256(code_verifier)));
params.set("code_challenge_method", "S256");
params.set("redirect_uri", redirectUrl);
params.set("scope", scopes.join(" "))
const tokenUrl = "https://api.getalby.com/oauth/token";
const authUrl = `https://getalby.com/oauth?${params}`;
return {
tokenUrl,
authUrl,
getToken: async (token: string) => {
const code = window.sessionStorage.getItem("alby-code");
if (!code) throw new Error("Alby code is missing!");
window.sessionStorage.removeItem("alby-code");
const form = new URLSearchParams();
form.set("client_id", clientId);
form.set("code_verifier", code);
form.set("grant_type", "authorization_code");
form.set("redirect_uri", redirectUrl);
form.set("code", token);
const req = await fetch(tokenUrl, {
method: "POST",
headers: {
"accept": "application/json",
"content-type": "application/x-www-form-urlencoded",
"authorization": `Basic ${base64.encode(ec.encode(`${clientId}:${clientSecret}`))}`
},
body: form
});
const data = await req.json();
if (req.ok) {
return data.access_token as string;
} else {
throw new Error(data.error_description as string);
}
}
};
}

View File

@ -0,0 +1,34 @@
import { RouteObject } from "react-router-dom";
import WalletSettings from "../WalletSettings";
import ConnectCashu from "./Cashu";
import ConnectLNC from "./LNC";
import ConnectLNDHub from "./LNDHub";
import ConnectNostrWallet from "./NWC";
import AlbyOAuth from "./Alby";
export const WalletSettingsRoutes = [
{
path: "/settings/wallet",
element: <WalletSettings />,
},
{
path: "/settings/wallet/lnc",
element: <ConnectLNC />,
},
{
path: "/settings/wallet/lndhub",
element: <ConnectLNDHub />,
},
{
path: "/settings/wallet/nwc",
element: <ConnectNostrWallet />,
},
{
path: "/settings/wallet/cashu",
element: <ConnectCashu />,
},
{
path: "/settings/wallet/alby",
element: <AlbyOAuth />,
}
] as Array<RouteObject>;