diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d2854ccf..f2e25bae 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,17 +1,27 @@ -import { RouterProvider, createHashRouter } from "react-router-dom"; +import { Navigate, RouterProvider, createHashRouter } from "react-router-dom"; import { ThemeProvider } from "src/components/ui/theme-provider"; import { usePosthog } from "./hooks/usePosthog"; import AppLayout from "src/components/layouts/AppLayout"; import SettingsLayout from "src/components/layouts/SettingsLayout"; +import TwoColumnFullScreenLayout from "src/components/layouts/TwoColumnFullScreenLayout"; import { DefaultRedirect } from "src/components/redirects/DefaultRedirect"; import { HomeRedirect } from "src/components/redirects/HomeRedirect"; +import { OnboardingRedirect } from "src/components/redirects/OnboardingRedirect"; +import { SetupRedirect } from "src/components/redirects/SetupRedirect"; +import { StartRedirect } from "src/components/redirects/StartRedirect"; import { Toaster } from "src/components/ui/toaster"; import { BackupMnemonic } from "src/screens/BackupMnemonic"; import { BackupNode } from "src/screens/BackupNode"; +import { BackupNodeSuccess } from "src/screens/BackupNodeSuccess"; import { Intro } from "src/screens/Intro"; import NotFound from "src/screens/NotFound"; +import Start from "src/screens/Start"; +import Unlock from "src/screens/Unlock"; +import { Welcome } from "src/screens/Welcome"; +import AlbyAuthRedirect from "src/screens/alby/AlbyAuthRedirect"; +import AppCreated from "src/screens/apps/AppCreated"; import AppList from "src/screens/apps/AppList"; import NewApp from "src/screens/apps/NewApp"; import ShowApp from "src/screens/apps/ShowApp"; @@ -19,6 +29,8 @@ import AppStore from "src/screens/appstore/AppStore"; import Channels from "src/screens/channels/Channels"; import { CurrentChannelOrder } from "src/screens/channels/CurrentChannelOrder"; import NewChannel from "src/screens/channels/NewChannel"; +import MigrateAlbyFunds from "src/screens/onboarding/MigrateAlbyFunds"; +import { Success } from "src/screens/onboarding/Success"; import BuyBitcoin from "src/screens/onchain/BuyBitcoin"; import DepositBitcoin from "src/screens/onchain/DepositBitcoin"; import ConnectPeer from "src/screens/peers/ConnectPeer"; @@ -26,6 +38,19 @@ import Peers from "src/screens/peers/Peers"; import { ChangeUnlockPassword } from "src/screens/settings/ChangeUnlockPassword"; import DebugTools from "src/screens/settings/DebugTools"; import Settings from "src/screens/settings/Settings"; +import { ImportMnemonic } from "src/screens/setup/ImportMnemonic"; +import { RestoreNode } from "src/screens/setup/RestoreNode"; +import { SetupAdvanced } from "src/screens/setup/SetupAdvanced"; +import { SetupFinish } from "src/screens/setup/SetupFinish"; +import { SetupNode } from "src/screens/setup/SetupNode"; +import { SetupPassword } from "src/screens/setup/SetupPassword"; +import { BreezForm } from "src/screens/setup/node/BreezForm"; +import { CashuForm } from "src/screens/setup/node/CashuForm"; +import { GreenlightForm } from "src/screens/setup/node/GreenlightForm"; +import { LDKForm } from "src/screens/setup/node/LDKForm"; +import { LNDForm } from "src/screens/setup/node/LNDForm"; +import { PhoenixdForm } from "src/screens/setup/node/PhoenixdForm"; +import { PresetNodeForm } from "src/screens/setup/node/PresetNodeForm"; import Wallet from "src/screens/wallet"; import SignMessage from "src/screens/wallet/SignMessage"; @@ -40,7 +65,7 @@ function App() { children: [ { index: true, - element: + element: , }, { path: "wallet", @@ -53,32 +78,35 @@ function App() { }, { path: "sign-message", - element: - - } - ] + element: , + handle: { crumb: () => "Sign Message" }, + }, + ], }, { path: "settings", element: , + handle: { crumb: () => "Settings" }, children: [ { index: true, - element: + element: , }, { path: "change-unlock-password", element: , + handle: { crumb: () => "Unlock Password" }, }, { path: "key-backup", element: , + handle: { crumb: () => "Key Backup" }, }, { path: "node-backup", element: , - } - ] + }, + ], }, { path: "apps", @@ -87,19 +115,23 @@ function App() { children: [ { index: true, - element: + element: , }, { path: ":pubkey", - element: + element: , }, { path: "new", element: , - handle: { crumb: () => "New App" } + handle: { crumb: () => "New App" }, + }, + { + path: "created", + element: , }, - ] + ], }, { path: "appstore", @@ -109,60 +141,193 @@ function App() { { index: true, element: , - handle: { crumb: () => "App Store" }, - } - ] + }, + ], }, { path: "channels", element: , + handle: { crumb: () => "Liquidity" }, children: [ { index: true, - element: + element: , }, { path: "new", - element: + element: , + handle: { crumb: () => "New Channel" }, }, { path: "order", - element: + element: , + handle: { crumb: () => "Current Order" }, }, { path: "onchain/buy-bitcoin", - element: + element: , + handle: { crumb: () => "Buy Bitcoin" }, }, { path: "onchain/deposit-bitcoin", - element: - } - ] - }, { + element: , + handle: { crumb: () => "Deposit Bitcoin" }, + }, + ], + }, + { path: "peers", element: , + handle: { crumb: () => "Peers" }, children: [ { index: true, - element: + element: , }, { path: "new", - element: - } - ] + element: , + handle: { crumb: () => "Connect Peer" }, + }, + ], }, { path: "debug-tools", element: , + handle: { crumb: () => "Debug" }, children: [ { index: true, - element: - } - ] + element: , + }, + ], + }, + ], + }, + { + element: , + children: [ + { + path: "start", + element: ( + + + + ), + }, + { + path: "alby/auth", + element: , + }, + { + path: "unlock", + element: , + }, + { + path: "welcome", + element: , + }, + { + path: "setup", + element: , + children: [ + { + element: , + }, + { + path: "password", + element: , + }, + { + path: "node", + children: [ + { + index: true, + element: , + }, + { + path: "breez", + element: , + }, + { + path: "greenlight", + element: , + }, + { + path: "cashu", + element: , + }, + { + path: "phoenix", + element: , + }, + { + path: "lnd", + element: , + }, + { + path: "ldk", + element: , + }, + { + path: "preset", + element: , + }, + ], + }, + { + path: "advanced", + element: , + }, + { + path: "import-mnemonic", + element: , + }, + { + path: "node-restore", + element: , + }, + { + path: "finish", + element: , + }, + ], }, - ] + { + path: "onboarding", + element: , + children: [ + { + path: "lightning/migrate-alby", + element: , + }, + { + path: "success", + element: , + }, + ], + }, + { + path: "alby/auth", + element: , + }, + ], + + /* + + }> + + } /> + + } /> + + + */ + }, + { + // TODO: Check if this is right here + path: "node-backup-success", + element: , }, { path: "intro", @@ -170,8 +335,8 @@ function App() { }, { path: "/*", - element: - } + element: , + }, ]); return ( @@ -179,51 +344,7 @@ function App() { - {/* - } - /> - }> - - - - } - /> - } /> - } /> - } /> - }> - } /> - } /> - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - } /> - } /> - } /> - } /> - - }> - - } /> - - } /> - - - - */} - + ); } diff --git a/frontend/src/components/AppHeader.tsx b/frontend/src/components/AppHeader.tsx index 913aaa96..a4e59af4 100644 --- a/frontend/src/components/AppHeader.tsx +++ b/frontend/src/components/AppHeader.tsx @@ -1,5 +1,5 @@ import { ReactElement } from "react"; -import Breadcrumbs from "src/components/Breadcrumb"; +import Breadcrumbs from "src/components/Breadcrumbs"; type Props = { title: string | ReactElement; @@ -9,14 +9,18 @@ type Props = { function AppHeader({ title, description, contentRight }: Props) { return ( - <> + <> +

{title}

-

{description}

+

+ {description} +

{contentRight}
-
+ + ); } diff --git a/frontend/src/components/Breadcrumb.tsx b/frontend/src/components/Breadcrumb.tsx deleted file mode 100644 index 84c723c8..00000000 --- a/frontend/src/components/Breadcrumb.tsx +++ /dev/null @@ -1,55 +0,0 @@ - -import { Link, useMatches } from "react-router-dom"; -import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbSeparator } from "src/components/ui/breadcrumb"; - - -function Breadcrumbs() { - const matches = useMatches(); - const crumbs = matches - // Skip the root item - //.slice(1) - // first get rid of any matches that don't have handle and crumb - .filter((match) => Boolean(match.handle?.crumb)) - // now map them into an array of elements, passing the loader - // data to each one - //.map((match) => match.handle.crumb(match.data)); - - //console.log(matches, crumbs); - - if (crumbs.length <= 1) { - return <>no breadcrumb; - } - - const isIndexRoute = crumbs.length >= 2 && crumbs[crumbs.length - 1].pathname ? crumbs[crumbs.length - 1].pathname.slice(0, -1) == crumbs[crumbs.length - 2].pathname : false; - - // Remove the last item if it's an index route to prevent e.g. Wallet > Wallet - const filteredCrumbs = isIndexRoute ? crumbs.slice(0, -1) : crumbs; - - //console.log("filtered", filteredCrumbs); - - return ( - <> - - - {filteredCrumbs.map((crumb, index) => ( - <> - - {index + 1 < filteredCrumbs.length ? - - {crumb.handle.crumb()} - - : - <>{crumb.handle.crumb()} - } - - {index + 1 < filteredCrumbs.length && } - - ))} - - - - - ); -} - -export default Breadcrumbs; diff --git a/frontend/src/components/Breadcrumbs.tsx b/frontend/src/components/Breadcrumbs.tsx new file mode 100644 index 00000000..99df860f --- /dev/null +++ b/frontend/src/components/Breadcrumbs.tsx @@ -0,0 +1,57 @@ +import { Link, useMatches } from "react-router-dom"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbSeparator, +} from "src/components/ui/breadcrumb"; + +function Breadcrumbs() { + const matches = useMatches(); + const crumbs = matches + // Skip the root item + .slice(1) + // first get rid of any matches that don't have handle and crumb + .filter((match) => Boolean(match.handle?.crumb)); + + // Compare pathnames of index routes to remove duplicates + const isIndexRoute = + crumbs.length >= 2 && crumbs[crumbs.length - 1].pathname + ? crumbs[crumbs.length - 1].pathname.slice(0, -1) == + crumbs[crumbs.length - 2].pathname + : false; + + // Remove the last item if it's an index route to prevent e.g. Wallet > Wallet + const filteredCrumbs = isIndexRoute ? crumbs.slice(0, -1) : crumbs; + + // Don't render anything if there is only one item + if (filteredCrumbs.length < 2) { + return; + } + + return ( + <> + + + {filteredCrumbs.map((crumb, index) => ( + <> + + {index + 1 < filteredCrumbs.length ? ( + + {crumb.handle.crumb()} + + ) : ( + <>{crumb.handle.crumb()} + )} + + {index + 1 < filteredCrumbs.length && } + + ))} + + + + ); +} + +export default Breadcrumbs; diff --git a/frontend/src/screens/apps/AppCreated.tsx b/frontend/src/screens/apps/AppCreated.tsx new file mode 100644 index 00000000..79f0ec8f --- /dev/null +++ b/frontend/src/screens/apps/AppCreated.tsx @@ -0,0 +1,147 @@ +import { CopyIcon } from "lucide-react"; +import { useEffect, useState } from "react"; +import { Link, Navigate, useLocation, useNavigate } from "react-router-dom"; + +import AppHeader from "src/components/AppHeader"; +import ExternalLink from "src/components/ExternalLink"; +import Loading from "src/components/Loading"; +import QRCode from "src/components/QRCode"; +import { suggestedApps } from "src/components/SuggestedAppData"; +import { Button } from "src/components/ui/button"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "src/components/ui/card"; +import { useToast } from "src/components/ui/use-toast"; +import { useApp } from "src/hooks/useApp"; +import { copyToClipboard } from "src/lib/clipboard"; +import { CreateAppResponse } from "src/types"; + +export default function AppCreated() { + const { search, state } = useLocation(); + const navigate = useNavigate(); + const { toast } = useToast(); + + const queryParams = new URLSearchParams(search); + const appId = queryParams.get("app") ?? ""; + const appstoreApp = suggestedApps.find((app) => app.id === appId); + console.info(appstoreApp, appId); + + const [timeout, setTimeout] = useState(false); + const createAppResponse = state as CreateAppResponse; + const pairingUri = createAppResponse.pairingUri; + const { data: app } = useApp(createAppResponse.pairingPublicKey, true); + + const copy = () => { + copyToClipboard(pairingUri); + toast({ title: "Copied to clipboard." }); + }; + + useEffect(() => { + const timeoutId = window.setTimeout(() => { + setTimeout(true); + }, 10000); + + return () => window.clearTimeout(timeoutId); + }, []); + + useEffect(() => { + if (app?.lastEventAt) { + toast({ + title: "Connection established!", + description: "You can now use the app with your Alby Hub.", + }); + navigate("/apps"); + } + }, [app?.lastEventAt, navigate, toast]); + + useEffect(() => { + if (appstoreApp) { + return; + } + // dispatch a success event which can be listened to by the opener or by the app that embedded the webview + // this gives those apps the chance to know the user has enabled the connection + const nwcEvent = new CustomEvent("nwc:success", { detail: {} }); + window.dispatchEvent(nwcEvent); + + // notify the opener of the successful connection + if (window.opener) { + window.opener.postMessage( + { + type: "nwc:success", + payload: { success: true }, + }, + "*" + ); + } + }, [appstoreApp]); + + if (!createAppResponse) { + return ; + } + + return ( + <> + +
+
+

+ 1. Open{" "} + {appstoreApp ? ( + + {appstoreApp.title} + + ) : ( + "the app you wish to connect" + )}{" "} + and look for a way to attach a wallet (most apps provide this option + in settings) +

+

2. Scan or paste the connection secret

+
+ + + Connection Secret + + +
+ +

Waiting for app to connect

+
+ {timeout && ( +
+ Connecting is taking longer than usual. + + + +
+ )} + + + {appstoreApp && ( + + )} + +
+ +
+
+
+
+ + ); +} diff --git a/frontend/src/screens/onchain/BuyBitcoin.tsx b/frontend/src/screens/onchain/BuyBitcoin.tsx index 4df04ee7..768e1b71 100644 --- a/frontend/src/screens/onchain/BuyBitcoin.tsx +++ b/frontend/src/screens/onchain/BuyBitcoin.tsx @@ -176,7 +176,7 @@ export default function BuyBitcoin() {

- How much bitcoin you’d like buy? + How much bitcoin would you like to buy?