feat: wallet connection flow
This commit is contained in:
@ -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} />,
|
||||
}}
|
||||
/>
|
||||
|
@ -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 (
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
98
packages/app/src/Pages/settings/wallet/Alby.tsx
Normal file
98
packages/app/src/Pages/settings/wallet/Alby.tsx
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
34
packages/app/src/Pages/settings/wallet/index.tsx
Normal file
34
packages/app/src/Pages/settings/wallet/index.tsx
Normal 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>;
|
Reference in New Issue
Block a user