From 4a539f1465345b700c1ee2ad069be44f16e9fe31 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Mon, 12 Feb 2024 15:01:58 +0530 Subject: [PATCH 01/22] feat: add mnemonic screen and other improvements --- frontend/index.html | 2 +- frontend/package.json | 2 + .../images => public}/nwc-brandmark.svg | 0 frontend/public/nwc-logo.svg | 1 - frontend/src/App.tsx | 4 + frontend/src/components/Alert.tsx | 22 +++ frontend/src/components/ConnectButton.tsx | 10 +- frontend/src/components/Input.tsx | 106 ++++++++++++ frontend/src/components/MnemonicInputs.tsx | 90 ++++++++++ frontend/src/components/PasswordAdornment.tsx | 39 +++++ frontend/src/index.css | 6 + frontend/src/screens/setup/SetupMnemonic.tsx | 154 ++++++++++++++++++ frontend/src/screens/setup/SetupNode.tsx | 115 +++++++------ frontend/src/screens/setup/SetupPassword.tsx | 17 +- frontend/src/screens/setup/SetupWallet.tsx | 65 ++++++++ frontend/src/utils/classes.ts | 3 + frontend/tailwind.config.js | 24 +-- frontend/yarn.lock | 23 +++ 18 files changed, 600 insertions(+), 83 deletions(-) rename frontend/{src/assets/images => public}/nwc-brandmark.svg (100%) delete mode 100644 frontend/public/nwc-logo.svg create mode 100644 frontend/src/components/Alert.tsx create mode 100644 frontend/src/components/Input.tsx create mode 100644 frontend/src/components/MnemonicInputs.tsx create mode 100644 frontend/src/components/PasswordAdornment.tsx create mode 100644 frontend/src/screens/setup/SetupMnemonic.tsx create mode 100644 frontend/src/screens/setup/SetupWallet.tsx create mode 100644 frontend/src/utils/classes.ts diff --git a/frontend/index.html b/frontend/index.html index 75cb15de..76876388 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ Alby - Nostr Wallet Connect - +
diff --git a/frontend/package.json b/frontend/package.json index 157ec13b..31fddd5f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,8 @@ "preview": "vite preview" }, "dependencies": { + "@bitcoin-design/bitcoin-icons-react": "^0.1.10", + "@scure/bip39": "^1.2.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1", diff --git a/frontend/src/assets/images/nwc-brandmark.svg b/frontend/public/nwc-brandmark.svg similarity index 100% rename from frontend/src/assets/images/nwc-brandmark.svg rename to frontend/public/nwc-brandmark.svg diff --git a/frontend/public/nwc-logo.svg b/frontend/public/nwc-logo.svg deleted file mode 100644 index d995567b..00000000 --- a/frontend/public/nwc-logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 52bc51bd..78e1f212 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,6 +11,7 @@ import NewApp from "src/screens/apps/NewApp"; import AppCreated from "src/screens/apps/AppCreated"; import NotFound from "src/screens/NotFound"; import { SetupNode } from "src/screens/setup/SetupNode"; +import { SetupWallet } from "src/screens/setup/SetupWallet"; import { Welcome } from "src/screens/Welcome"; import { SetupPassword } from "src/screens/setup/SetupPassword"; import Start from "src/screens/Start"; @@ -19,6 +20,7 @@ import { StartRedirect } from "src/components/redirects/StartRedirect"; import { HomeRedirect } from "src/components/redirects/HomeRedirect"; import Unlock from "src/screens/Unlock"; import { SetupRedirect } from "src/components/redirects/SetupRedirect"; +import { SetupMnemonic } from "src/screens/setup/SetupMnemonic"; function App() { return ( @@ -41,6 +43,8 @@ function App() { } /> } /> } /> + } /> + } /> }> } /> diff --git a/frontend/src/components/Alert.tsx b/frontend/src/components/Alert.tsx new file mode 100644 index 00000000..b269f2f6 --- /dev/null +++ b/frontend/src/components/Alert.tsx @@ -0,0 +1,22 @@ +import { classNames } from "src/utils/classes"; + +type Props = { + type: "warn" | "info"; + children: React.ReactNode; +}; + +export default function Alert({ type, children }: Props) { + return ( +
+ {children} +
+ ); +} diff --git a/frontend/src/components/ConnectButton.tsx b/frontend/src/components/ConnectButton.tsx index 56b13510..21c3865e 100644 --- a/frontend/src/components/ConnectButton.tsx +++ b/frontend/src/components/ConnectButton.tsx @@ -4,20 +4,24 @@ type ConnectButtonProps = { isConnecting: boolean; loadingText?: string; submitText?: string; + disabled?: boolean; }; export default function ConnectButton({ isConnecting, loadingText, submitText, + disabled, }: ConnectButtonProps) { return ( + ); +} diff --git a/frontend/src/index.css b/frontend/src/index.css index 0f00041a..21e067ca 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -13,3 +13,9 @@ body, flex-direction: column; flex: 1; } + +/* Removes an arrow on the datalist of word suggestions when you are typing a word in the import secret key page. + See https://stackoverflow.com/questions/20937475/remove-datalist-dropdown-arrow-in-chrome */ +input[type="text"]::-webkit-calendar-picker-indicator { + display: none !important; +} diff --git a/frontend/src/screens/setup/SetupMnemonic.tsx b/frontend/src/screens/setup/SetupMnemonic.tsx new file mode 100644 index 00000000..f1898c91 --- /dev/null +++ b/frontend/src/screens/setup/SetupMnemonic.tsx @@ -0,0 +1,154 @@ +import { + AlertCircleIcon, + BuoyIcon, + ShieldIcon, +} from "@bitcoin-design/bitcoin-icons-react/outline"; +import { useState } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; + +import { useInfo } from "src/hooks/useInfo"; +import * as bip39 from "@scure/bip39"; +import { wordlist } from "@scure/bip39/wordlists/english"; +import MnemonicInputs from "src/components/MnemonicInputs"; +import ConnectButton from "src/components/ConnectButton"; +import { useCSRF } from "src/hooks/useCSRF"; +import { handleRequestError } from "src/utils/handleRequestError"; +import { request } from "src/utils/request"; +import Alert from "src/components/Alert"; + +export function SetupMnemonic() { + const navigate = useNavigate(); + const { state, search } = useLocation(); + const params = new URLSearchParams(search); + const isNew = params.get("wallet") === "new"; + + const data = state as object; + + const [mnemonic, setMnemonic] = useState( + isNew ? bip39.generateMnemonic(wordlist, 128) : "" + ); + const [backedUp, isBackedUp] = useState(false); + const [isConnecting, setConnecting] = useState(false); + + const { data: info, mutate: refetchInfo } = useInfo(); + const { data: csrf } = useCSRF(); + + async function onSubmit(e: React.FormEvent) { + e.preventDefault(); + if ( + mnemonic.split(" ").length !== 12 || + !bip39.validateMnemonic(mnemonic, wordlist) + ) { + alert("Invalid mnemonic"); + return; + } + + try { + setConnecting(true); + if (!csrf) { + throw new Error("info not loaded"); + } + await request("/api/setup", { + method: "POST", + headers: { + "X-CSRF-Token": csrf, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + breezMnemonic: mnemonic, + ...data, + }), + }); + + await refetchInfo(); + navigate("/"); + } catch (error) { + handleRequestError("Failed to connect", error); + } finally { + setConnecting(false); + } + } + + return ( + <> +
+

+ {isNew ? "Back up your wallet" : "Enter your mnemonic"} +

+ {info?.setupCompleted && ( + + ⚠️ Your node is already setup! only continue if you actually want to + change your connection settings. + + )} + {/* Think of a back button */} + + {isNew && ( + <> +
+
+ +
+ + Recovery phrase is a set of 12 words that backs up your wallet + +
+
+
+ +
+ + Make sure to write them down somewhere safe and private + +
+
+
+ +
+ + If you lose your recovery phrase, you will lose access to your + funds + +
+ + )} + + + {isNew && ( +
+ { + isBackedUp(event.target.checked); + }} + checked={backedUp} + className="w-4 h-4 text-purple-700 bg-gray-100 border-gray-300 rounded focus:ring-purple-700 dark:focus:ring-purple-800 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" + /> + +
+ )} +
+ + + + ); +} diff --git a/frontend/src/screens/setup/SetupNode.tsx b/frontend/src/screens/setup/SetupNode.tsx index 2543a3ff..b7796e01 100644 --- a/frontend/src/screens/setup/SetupNode.tsx +++ b/frontend/src/screens/setup/SetupNode.tsx @@ -1,5 +1,6 @@ import React from "react"; -import { useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; +import Alert from "src/components/Alert"; import ConnectButton from "src/components/ConnectButton"; import Container from "src/components/Container"; import { useCSRF } from "src/hooks/useCSRF"; @@ -14,16 +15,32 @@ export function SetupNode() { const { unlockPassword } = useSetupStore(); const [isConnecting, setConnecting] = React.useState(false); const navigate = useNavigate(); + const location = useLocation(); + + const params = new URLSearchParams(location.search); + const isNew = params.get("wallet") === "new"; const { data: info, mutate: refetchInfo } = useInfo(); const { data: csrf } = useCSRF(); async function handleSubmit(data: object) { + if (backendType === "BREEZ") { + navigate(`/setup/mnemonic${isNew ? "?wallet=new" : ""}`, { + state: { + backendType, + unlockPassword, + ...data, + }, + }); + return; + } + try { setConnecting(true); if (!csrf) { throw new Error("info not loaded"); } + await request("/api/setup", { method: "POST", headers: { @@ -49,39 +66,43 @@ export function SetupNode() { return ( <> -

+

Enter your node connection credentials to connect to your wallet.

- {info?.setupCompleted && ( -

- Your node is already setup! only continue if you actually want to + + ⚠️ Your node is already setup! only continue if you actually want to change your connection settings. -

- )} - - - - {backendType === "BREEZ" && ( - - )} - {backendType === "LND" && ( - + )} +
+ + + {backendType === "BREEZ" && ( + + )} + {backendType === "LND" && ( + + )} +
); @@ -96,18 +117,17 @@ function BreezForm({ isConnecting, handleSubmit }: SetupFormProps) { const [greenlightInviteCode, setGreenlightInviteCode] = React.useState(""); const [breezApiKey, setBreezApiKey] = React.useState(""); - const [breezMnemonic, setBreezMnemonic] = React.useState(""); function onSubmit(e: React.FormEvent) { e.preventDefault(); - if (!greenlightInviteCode || !breezMnemonic) { - alert("please fill out all fields"); + // Isn't breezApiKey not required? + if (!greenlightInviteCode) { + alert("Please fill out all fields"); return; } handleSubmit({ greenlightInviteCode, breezApiKey, - breezMnemonic, }); } @@ -116,7 +136,7 @@ function BreezForm({ isConnecting, handleSubmit }: SetupFormProps) { <> @@ -126,11 +146,13 @@ function BreezForm({ isConnecting, handleSubmit }: SetupFormProps) { value={greenlightInviteCode} type="password" id="greenlight-invite-code" + autoComplete="new-password" + placeholder="XXXX-YYYY" className="dark:bg-surface-00dp block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:ring-2 focus:ring-purple-700 dark:border-gray-700 dark:text-white dark:placeholder-gray-400 dark:ring-offset-gray-800 dark:focus:ring-purple-600" /> @@ -140,20 +162,7 @@ function BreezForm({ isConnecting, handleSubmit }: SetupFormProps) { value={breezApiKey} type="password" id="breez-api-key" - className="dark:bg-surface-00dp block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:ring-2 focus:ring-purple-700 dark:border-gray-700 dark:text-white dark:placeholder-gray-400 dark:ring-offset-gray-800 dark:focus:ring-purple-600" - /> - - setBreezMnemonic(e.target.value)} - value={breezMnemonic} - type="password" - id="mnemonic" + autoComplete="new-password" className="dark:bg-surface-00dp block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:ring-2 focus:ring-purple-700 dark:border-gray-700 dark:text-white dark:placeholder-gray-400 dark:ring-offset-gray-800 dark:focus:ring-purple-600" /> @@ -190,7 +199,7 @@ function LNDForm({ isConnecting, handleSubmit }: SetupFormProps) { <> @@ -204,7 +213,7 @@ function LNDForm({ isConnecting, handleSubmit }: SetupFormProps) { @@ -218,7 +227,7 @@ function LNDForm({ isConnecting, handleSubmit }: SetupFormProps) { /> @@ -233,7 +242,7 @@ function LNDForm({ isConnecting, handleSubmit }: SetupFormProps) { diff --git a/frontend/src/screens/setup/SetupPassword.tsx b/frontend/src/screens/setup/SetupPassword.tsx index cb855c16..9f26638d 100644 --- a/frontend/src/screens/setup/SetupPassword.tsx +++ b/frontend/src/screens/setup/SetupPassword.tsx @@ -3,8 +3,8 @@ import { useNavigate } from "react-router-dom"; import { useInfo } from "src/hooks/useInfo"; import useSetupStore from "src/state/SetupStore"; -import nwcComboMark from "src/assets/images/nwc-combomark.svg"; import Container from "src/components/Container"; +import Alert from "src/components/Alert"; export function SetupPassword() { const store = useSetupStore(); @@ -18,24 +18,23 @@ export function SetupPassword() { alert("Passwords don't match!"); return; } - navigate("/setup/node"); + navigate("/setup/wallet"); } return ( <>
-

+

Choose a password to unlock the NWC app

{info?.setupCompleted && ( -

- Your node is already setup! only continue if you actually want to - change your connection settings. -

+ + ⚠️ Your node is already setup! only continue if you actually want + to change your connection settings. + )} - -
+
- Recovery phrase is a set of 12 words that backs up your wallet + Your recovery phrase is a set of 12 words that backs up your + wallet
diff --git a/frontend/src/screens/setup/SetupWallet.tsx b/frontend/src/screens/setup/SetupWallet.tsx index d6afd8d5..e139fe19 100644 --- a/frontend/src/screens/setup/SetupWallet.tsx +++ b/frontend/src/screens/setup/SetupWallet.tsx @@ -54,7 +54,9 @@ function WalletComponent({ walletType }: { walletType: string }) { {walletType} Wallet
- {walletType == "new" ? "Breez only" : "Breez or LND"} + {walletType == "new" + ? "Create a new wallet powered by the Breez SDK" + : "Connect to an existing Breez or LND wallet"}
From cf62444e1496b87e6d1500213b77fc125b78f458 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Wed, 14 Feb 2024 11:08:08 +0530 Subject: [PATCH 04/22] chore: refactoring --- breez.go | 2 +- frontend/src/screens/setup/SetupMnemonic.tsx | 27 ++++++++-- frontend/src/screens/setup/SetupNode.tsx | 56 +++++++++++--------- frontend/src/screens/setup/SetupPassword.tsx | 3 +- frontend/src/state/SetupStore.ts | 5 ++ frontend/src/types.ts | 13 +++++ service.go | 2 +- 7 files changed, 77 insertions(+), 31 deletions(-) diff --git a/breez.go b/breez.go index bc77249b..db4cb952 100644 --- a/breez.go +++ b/breez.go @@ -31,7 +31,7 @@ func (BreezListener) OnEvent(e breez_sdk.BreezEvent) { } func NewBreezService(mnemonic, apiKey, inviteCode, workDir string) (result LNClient, err error) { - if mnemonic == "" || apiKey == "" || inviteCode == "" || workDir == "" { + if mnemonic == "" || apiKey == "" || workDir == "" { return nil, errors.New("One or more required breez configuration are missing") } diff --git a/frontend/src/screens/setup/SetupMnemonic.tsx b/frontend/src/screens/setup/SetupMnemonic.tsx index 716eb5b5..825f40d7 100644 --- a/frontend/src/screens/setup/SetupMnemonic.tsx +++ b/frontend/src/screens/setup/SetupMnemonic.tsx @@ -15,6 +15,7 @@ import { useCSRF } from "src/hooks/useCSRF"; import { handleRequestError } from "src/utils/handleRequestError"; import { request } from "src/utils/request"; import Alert from "src/components/Alert"; +import toast from "src/components/Toast"; export function SetupMnemonic() { const navigate = useNavigate(); @@ -39,7 +40,7 @@ export function SetupMnemonic() { mnemonic.split(" ").length !== 12 || !bip39.validateMnemonic(mnemonic, wordlist) ) { - alert("Invalid mnemonic"); + toast.error("Invalid recovery phrase"); return; } @@ -76,7 +77,7 @@ export function SetupMnemonic() { className="flex flex-col gap-2 mx-auto max-w-2xl text-sm" >

- {isNew ? "Back up your wallet" : "Enter your mnemonic"} + {isNew ? "Back up your wallet" : "Import your wallet"}

{info?.setupCompleted && ( @@ -86,7 +87,7 @@ export function SetupMnemonic() { )} {/* Think of a back button */} - {isNew && ( + {isNew ? ( <>
@@ -115,6 +116,26 @@ export function SetupMnemonic() {
+ ) : ( + <> +
+
+ +
+ + Recovery phrase is a set of 12 words that restores your wallet + from a backup + +
+
+
+ +
+ + Make sure to enter them somewhere safe and private + +
+ )} setBackendType(e.target.value as BackendType)} id="backend-type" className="dark:bg-surface-00dp mb-4 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:ring-2 focus:ring-purple-700 dark:border-gray-700 dark:text-white dark:placeholder-gray-400 dark:ring-offset-gray-800 dark:focus:ring-purple-600" > - + {!isNew && } {backendType === "BREEZ" && ( )} {backendType === "LND" && ( @@ -113,16 +114,19 @@ type SetupFormProps = { handleSubmit(data: unknown): void; }; -function BreezForm({ isConnecting, handleSubmit }: SetupFormProps) { +type BreezFormProps = SetupFormProps & { + isNew: boolean; +}; + +function BreezForm({ isConnecting, handleSubmit, isNew }: BreezFormProps) { const [greenlightInviteCode, setGreenlightInviteCode] = React.useState(""); const [breezApiKey, setBreezApiKey] = React.useState(""); function onSubmit(e: React.FormEvent) { e.preventDefault(); - // Isn't breezApiKey not required? - if (!greenlightInviteCode) { - alert("Please fill out all fields"); + if ((isNew && !greenlightInviteCode) || !breezApiKey) { + toast.error("Please fill out all fields"); return; } handleSubmit({ @@ -134,22 +138,25 @@ function BreezForm({ isConnecting, handleSubmit }: SetupFormProps) { return ( <> - - setGreenlightInviteCode(e.target.value)} - value={greenlightInviteCode} - type="password" - id="greenlight-invite-code" - autoComplete="new-password" - placeholder="XXXX-YYYY" - className="dark:bg-surface-00dp block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:ring-2 focus:ring-purple-700 dark:border-gray-700 dark:text-white dark:placeholder-gray-400 dark:ring-offset-gray-800 dark:focus:ring-purple-600" - /> + {isNew && ( + <> + + setGreenlightInviteCode(e.target.value)} + value={greenlightInviteCode} + type="text" + id="greenlight-invite-code" + placeholder="XXXX-YYYY" + className="dark:bg-surface-00dp block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:ring-2 focus:ring-purple-700 dark:border-gray-700 dark:text-white dark:placeholder-gray-400 dark:ring-offset-gray-800 dark:focus:ring-purple-600" + /> + + )}