From 4d9226b3b6fa3089e38da5a1e60b9bae8302e451 Mon Sep 17 00:00:00 2001 From: Kieran Date: Wed, 3 Jan 2024 16:46:18 +0000 Subject: [PATCH] feat: wallet connection flow --- packages/app/src/Icons/Alby.tsx | 19 ++++ packages/app/src/Icons/Cashu.tsx | 45 ++++++++ packages/app/src/Pages/WalletPage.tsx | 4 +- packages/app/src/Pages/settings/Routes.tsx | 2 +- .../app/src/Pages/settings/WalletSettings.css | 14 --- .../app/src/Pages/settings/WalletSettings.tsx | 101 ++++++++++-------- .../app/src/Pages/settings/wallet/Alby.tsx | 98 +++++++++++++++++ .../app/src/Pages/settings/wallet/index.tsx | 34 ++++++ 8 files changed, 253 insertions(+), 64 deletions(-) create mode 100644 packages/app/src/Icons/Alby.tsx create mode 100644 packages/app/src/Icons/Cashu.tsx delete mode 100644 packages/app/src/Pages/settings/WalletSettings.css create mode 100644 packages/app/src/Pages/settings/wallet/Alby.tsx create mode 100644 packages/app/src/Pages/settings/wallet/index.tsx diff --git a/packages/app/src/Icons/Alby.tsx b/packages/app/src/Icons/Alby.tsx new file mode 100644 index 00000000..df6723b5 --- /dev/null +++ b/packages/app/src/Icons/Alby.tsx @@ -0,0 +1,19 @@ +export default function AlbyIcon(props: { size?: number }) { + return + + + + + + + + + + + + + + + + ; +} \ No newline at end of file diff --git a/packages/app/src/Icons/Cashu.tsx b/packages/app/src/Icons/Cashu.tsx new file mode 100644 index 00000000..d30a3a4e --- /dev/null +++ b/packages/app/src/Icons/Cashu.tsx @@ -0,0 +1,45 @@ +export default function CashuIcon(props: { size?: number }) { + return + + + + + + + + +} \ No newline at end of file diff --git a/packages/app/src/Pages/WalletPage.tsx b/packages/app/src/Pages/WalletPage.tsx index c1bae0c4..b3d8c534 100644 --- a/packages/app/src/Pages/WalletPage.tsx +++ b/packages/app/src/Pages/WalletPage.tsx @@ -191,8 +191,8 @@ export default function WalletPage(props: { showHistory: boolean }) { defaultMessage="{amount} sats" id="E5ZIPD" values={{ - big: c => {c}, - small: c => {c}, + big: c => {c}, + small: c => {c}, amount: , }} /> diff --git a/packages/app/src/Pages/settings/Routes.tsx b/packages/app/src/Pages/settings/Routes.tsx index 7b81020d..6083e6b4 100644 --- a/packages/app/src/Pages/settings/Routes.tsx +++ b/packages/app/src/Pages/settings/Routes.tsx @@ -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 ( diff --git a/packages/app/src/Pages/settings/WalletSettings.css b/packages/app/src/Pages/settings/WalletSettings.css deleted file mode 100644 index 4d2f85b9..00000000 --- a/packages/app/src/Pages/settings/WalletSettings.css +++ /dev/null @@ -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; -} diff --git a/packages/app/src/Pages/settings/WalletSettings.tsx b/packages/app/src/Pages/settings/WalletSettings.tsx index aea1e08f..8db145d6 100644 --- a/packages/app/src/Pages/settings/WalletSettings.tsx +++ b/packages/app/src/Pages/settings/WalletSettings.tsx @@ -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
{ + if (props.url.startsWith("http")) { + window.location.href = props.url; + } else { + navigate(props.url); + } + }}> +
+ {props.logo} +
+
+
{props.name}
+
{props.desc}
+
+ +
+} const WalletSettings = () => { - const navigate = useNavigate(); + const alby = getAlbyOAuth(); return ( <> -

-
-
navigate("/settings/wallet/lnc")}> - -

LND with LNC

-
-
navigate("/settings/wallet/lndhub")}> - -

LNDHub

-
-
navigate("/settings/wallet/nwc")}> - -

Nostr Wallet Connect

-
+
+ } + name="Nostr Wallet Connect" + url="/settings/wallet/nwc" + desc={} /> + } + name="LND via LNC" + url="/settings/wallet/lnc" + desc={} /> + } + name="LNDHub" + url="/settings/wallet/lndhub" + desc={} /> + } + name="Cashu" + url="/settings/wallet/cashu" + desc={} /> + } + name="Alby" + url={alby.authUrl} + desc={} />
); }; -export default WalletSettings; - -export const WalletSettingsRoutes = [ - { - path: "/settings/wallet", - element: , - }, - { - path: "/settings/wallet/lnc", - element: , - }, - { - path: "/settings/wallet/lndhub", - element: , - }, - { - path: "/settings/wallet/nwc", - element: , - }, - { - path: "/settings/wallet/cashu", - element: , - }, -] as Array; +export default WalletSettings; \ No newline at end of file diff --git a/packages/app/src/Pages/settings/wallet/Alby.tsx b/packages/app/src/Pages/settings/wallet/Alby.tsx new file mode 100644 index 00000000..ff5b6559 --- /dev/null +++ b/packages/app/src/Pages/settings/wallet/Alby.tsx @@ -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 <> +

Alby Wallet Connection

+ {!error && } + {error && {error}} + +} + +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); + } + } + }; +} \ No newline at end of file diff --git a/packages/app/src/Pages/settings/wallet/index.tsx b/packages/app/src/Pages/settings/wallet/index.tsx new file mode 100644 index 00000000..5716890d --- /dev/null +++ b/packages/app/src/Pages/settings/wallet/index.tsx @@ -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: , + }, + { + path: "/settings/wallet/lnc", + element: , + }, + { + path: "/settings/wallet/lndhub", + element: , + }, + { + path: "/settings/wallet/nwc", + element: , + }, + { + path: "/settings/wallet/cashu", + element: , + }, + { + path: "/settings/wallet/alby", + element: , + } +] as Array; \ No newline at end of file