From f85d2173561fbf30bcda7c1a3d6cf3804ceda7ed Mon Sep 17 00:00:00 2001 From: im-adithya Date: Mon, 3 Jun 2024 19:54:15 +0530 Subject: [PATCH 1/4] chore: merge app connection flows --- frontend/src/App.tsx | 4 - frontend/src/components/SuggestedApps.tsx | 2 +- frontend/src/screens/apps/AppCreated.tsx | 162 ++++++++++++------- frontend/src/screens/apps/NewApp.tsx | 39 +++-- frontend/src/screens/appstore/AppConnect.tsx | 122 -------------- frontend/src/screens/appstore/AppDetail.tsx | 107 ------------ 6 files changed, 132 insertions(+), 304 deletions(-) delete mode 100644 frontend/src/screens/appstore/AppConnect.tsx delete mode 100644 frontend/src/screens/appstore/AppDetail.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9d698e11..8acb296d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -39,8 +39,6 @@ import { BackupNode } from "src/screens/BackupNode"; import { BackupNodeSuccess } from "src/screens/BackupNodeSuccess"; import { Intro } from "src/screens/Intro"; import AlbyAuthRedirect from "src/screens/alby/AlbyAuthRedirect"; -import AppConnect from "src/screens/appstore/AppConnect"; -import AppDetail from "src/screens/appstore/AppDetail"; import { CurrentChannelOrder } from "src/screens/channels/CurrentChannelOrder"; import { Success } from "src/screens/onboarding/Success"; import Peers from "src/screens/peers/Peers"; @@ -79,8 +77,6 @@ function App() { }> } /> - } /> - } /> }> } /> diff --git a/frontend/src/components/SuggestedApps.tsx b/frontend/src/components/SuggestedApps.tsx index 100790ce..77d52c24 100644 --- a/frontend/src/components/SuggestedApps.tsx +++ b/frontend/src/components/SuggestedApps.tsx @@ -9,7 +9,7 @@ import { SuggestedApp, suggestedApps } from "./SuggestedAppData"; function SuggestedAppCard({ id, title, description, logo }: SuggestedApp) { return ( - +
diff --git a/frontend/src/screens/apps/AppCreated.tsx b/frontend/src/screens/apps/AppCreated.tsx index 3b5a1265..e2643ab7 100644 --- a/frontend/src/screens/apps/AppCreated.tsx +++ b/frontend/src/screens/apps/AppCreated.tsx @@ -1,24 +1,61 @@ -import { DialogDescription, DialogTrigger } from "@radix-ui/react-dialog"; -import { CopyIcon, QrCode } from "lucide-react"; -import { useEffect } from "react"; -import { Navigate, useLocation } from "react-router-dom"; +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 { - Dialog, - DialogContent, - DialogHeader, - DialogTitle -} from "src/components/ui/dialog"; + 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 { state } = useLocation(); + 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(() => { // dispatch a success event which can be listened to by the opener or by the app that embedded the webview @@ -42,57 +79,66 @@ export default function AppCreated() { return ; } - const pairingUri = createAppResponse.pairingUri; - - const copy = () => { - copyToClipboard(pairingUri); - toast({ title: "Copied to clipboard." }); - }; - return ( -
-

- 🚀 Almost there! -

-
- Complete the last step of the setup by pasting or scanning your - connection's pairing secret in the desired app to finalise the - connection. -
- -
- - - - - - - - - - Scan QR Code - - - Open the app you want to pair and scan this QR code to connect. - - -
- + +
+
+

+ 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/apps/NewApp.tsx b/frontend/src/screens/apps/NewApp.tsx index 177ceb6c..a4a5406f 100644 --- a/frontend/src/screens/apps/NewApp.tsx +++ b/frontend/src/screens/apps/NewApp.tsx @@ -20,6 +20,7 @@ import { useToast } from "src/components/ui/use-toast"; import { handleRequestError } from "src/utils/handleRequestError"; import { request } from "src/utils/request"; // build the project for this to appear import Permissions from "../../components/Permissions"; +import { suggestedApps } from "../../components/SuggestedAppData"; const NewApp = () => { const location = useLocation(); @@ -28,11 +29,17 @@ const NewApp = () => { const navigate = useNavigate(); const queryParams = new URLSearchParams(location.search); - const nameParam = (queryParams.get("name") || queryParams.get("c")) ?? ""; + + const appId = queryParams.get("app") ?? ""; + const app = suggestedApps.find((app) => app.id === appId); + + const nameParam = app + ? app.title + : (queryParams.get("name") || queryParams.get("c")) ?? ""; const pubkey = queryParams.get("pubkey") ?? ""; const returnTo = queryParams.get("return_to") ?? ""; - const [appName, setAppName] = useState(() => nameParam); + const [appName, setAppName] = useState(nameParam); const budgetRenewalParam = queryParams.get( "budget_renewal" @@ -102,7 +109,7 @@ const NewApp = () => { window.location.href = createAppResponse.returnTo; return; } - navigate("/apps/created", { + navigate(`/apps/created${app ? `?app=${app.id}` : ""}`, { state: createAppResponse, }); toast({ title: "App created" }); @@ -117,12 +124,20 @@ const NewApp = () => { title={nameParam ? `Connect to ${appName}` : "Connect a new app"} description="Configure wallet permissions for the app and follow instructions to finalise the connection" /> -
+ + {app && ( +
+ +

{app.title}

+
+ )} {!nameParam && (
- + { id="name" onChange={(e) => setAppName(e.target.value)} required - autoComplete="off" /> + autoComplete="off" + />

Name of the app or purpose of the connection

@@ -143,14 +159,13 @@ const NewApp = () => { initialPermissions={permissions} onPermissionsChange={setPermissions} isEditing={!reqMethodsParam} - isNew /> + isNew + />
- + ); diff --git a/frontend/src/screens/appstore/AppConnect.tsx b/frontend/src/screens/appstore/AppConnect.tsx deleted file mode 100644 index d9ed5c52..00000000 --- a/frontend/src/screens/appstore/AppConnect.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { CopyIcon } from "lucide-react"; -import { useEffect, useState } from "react"; -import { - Link, - Navigate, - useLocation, - useNavigate, - useParams, -} 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 AppConnect() { - const { state } = useLocation(); - const navigate = useNavigate(); - const { toast } = useToast(); - const params = useParams(); - const [timeout, setTimeout] = useState(false); - const createAppResponse = state as CreateAppResponse; - const appstoreApp = suggestedApps.find((x) => x.id == params.id); - const { data: app } = useApp(createAppResponse.pairingPublicKey, true); - const pairingUri = createAppResponse.pairingUri; - - 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]); - - if (!createAppResponse || !appstoreApp) { - return ; - } - - return ( - <> - -
-
-

- 1. Open{" "} - - {appstoreApp.title} - {" "} - 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. - - - -
- )} - - - - -
- -
-
-
-
- - ); -} diff --git a/frontend/src/screens/appstore/AppDetail.tsx b/frontend/src/screens/appstore/AppDetail.tsx deleted file mode 100644 index a2dbce86..00000000 --- a/frontend/src/screens/appstore/AppDetail.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; -import AppHeader from "src/components/AppHeader"; -import { suggestedApps } from "src/components/SuggestedAppData"; -import { Button } from "src/components/ui/button"; -import { Separator } from "src/components/ui/separator"; -import { toast } from "src/components/ui/use-toast"; -import { useCSRF } from "src/hooks/useCSRF"; -import { - AppPermissions, - CreateAppResponse, - PermissionType, - nip47PermissionDescriptions, -} from "src/types"; -import { handleRequestError } from "src/utils/handleRequestError"; -import { request } from "src/utils/request"; -import Permissions from "../../components/Permissions"; - -export default function AppDetail() { - const params = useParams(); - const navigate = useNavigate(); - const { data: csrf } = useCSRF(); - - const app = suggestedApps.find((x) => x.id == params.id); - - const methods = Object.keys(nip47PermissionDescriptions); - const requestMethodsSet = new Set( - methods as PermissionType[] - ); - - const [permissions, setPermissions] = useState({ - requestMethods: requestMethodsSet, - maxAmount: 100000, - budgetRenewal: "monthly", - }); - - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - if (!csrf) { - throw new Error("No CSRF token"); - } - - try { - const createAppResponse = await request("/api/apps", { - method: "POST", - headers: { - "X-CSRF-Token": csrf, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: app?.title, - ...permissions, - requestMethods: [...permissions.requestMethods].join(" "), - expiresAt: permissions.expiresAt?.toISOString(), - }), - }); - - if (!createAppResponse) { - throw new Error("no create app response received"); - } - - navigate("/appstore/" + app?.id + "/connect", { - state: createAppResponse, - }); - toast({ title: "App created" }); - } catch (error) { - handleRequestError(toast, "Failed to create app", error); - } - }; - - if (!app) { - return; - } - - return ( - <> - -
-
- -

{app.title}

-
- -
-

Authorize the app to:

- -
- - - - - - - ); -} From 3e064a8d1b04d91c894e183b76271df27b02458d Mon Sep 17 00:00:00 2001 From: im-adithya Date: Tue, 4 Jun 2024 11:37:09 +0530 Subject: [PATCH 2/4] chore: don't dispatch event for store apps --- frontend/src/screens/apps/AppCreated.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/screens/apps/AppCreated.tsx b/frontend/src/screens/apps/AppCreated.tsx index e2643ab7..79f0ec8f 100644 --- a/frontend/src/screens/apps/AppCreated.tsx +++ b/frontend/src/screens/apps/AppCreated.tsx @@ -58,6 +58,9 @@ export default function AppCreated() { }, [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: {} }); @@ -73,7 +76,7 @@ export default function AppCreated() { "*" ); } - }, []); + }, [appstoreApp]); if (!createAppResponse) { return ; From c7e3ce3db3da482c3c5f742f9606e4992c2625a3 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Thu, 6 Jun 2024 17:41:38 +0530 Subject: [PATCH 3/4] chore: rename props and add return to --- frontend/src/components/Permissions.tsx | 41 ++++++++++++++----------- frontend/src/screens/apps/NewApp.tsx | 11 +++++-- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/Permissions.tsx b/frontend/src/components/Permissions.tsx index ff60ff57..4aee0116 100644 --- a/frontend/src/components/Permissions.tsx +++ b/frontend/src/components/Permissions.tsx @@ -26,20 +26,20 @@ interface PermissionsProps { initialPermissions: AppPermissions; onPermissionsChange: (permissions: AppPermissions) => void; budgetUsage?: number; - isEditing: boolean; - isNew?: boolean; + canEditPermissions: boolean; + isNewConnection?: boolean; } const Permissions: React.FC = ({ initialPermissions, onPermissionsChange, - isEditing, - isNew, + canEditPermissions, + isNewConnection, budgetUsage, }) => { const [permissions, setPermissions] = React.useState(initialPermissions); - const [days, setDays] = useState(isNew ? 0 : -1); - const [expireOptions, setExpireOptions] = useState(!isNew); + const [days, setDays] = useState(isNewConnection ? 0 : -1); + const [expireOptions, setExpireOptions] = useState(!isNewConnection); useEffect(() => { setPermissions(initialPermissions); @@ -54,7 +54,7 @@ const Permissions: React.FC = ({ }; const handleRequestMethodChange = (requestMethod: PermissionType) => { - if (!isEditing) { + if (!canEditPermissions) { return; } @@ -109,7 +109,7 @@ const Permissions: React.FC = ({ className={cn( "w-full", rm == "pay_invoice" ? "order-last" : "", - !isEditing && !permissions.requestMethods.has(rm) + !canEditPermissions && !permissions.requestMethods.has(rm) ? "hidden" : "" )} @@ -119,19 +119,22 @@ const Permissions: React.FC = ({ )} handleRequestMethodChange(rm)} checked={permissions.requestMethods.has(rm)} /> @@ -141,23 +144,23 @@ const Permissions: React.FC = ({ className={cn( "pt-2 pb-2 pl-5 ml-2.5 border-l-2 border-l-primary", !permissions.requestMethods.has(rm) - ? isEditing + ? canEditPermissions ? "pointer-events-none opacity-30" : "hidden" : "" )} > - {isEditing ? ( + {canEditPermissions ? ( <>

Budget Renewal:

- {!isEditing ? ( + {!canEditPermissions ? ( permissions.budgetRenewal ) : ( {
+ {returnTo && ( +

+ You will automatically return to {returnTo} +

+ )} From 89eb5fe0b996972b03d406ff8c9d18c60dd03d56 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Thu, 6 Jun 2024 19:58:20 +0530 Subject: [PATCH 4/4] chore: change prop name in ShowApp --- frontend/src/screens/apps/ShowApp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/screens/apps/ShowApp.tsx b/frontend/src/screens/apps/ShowApp.tsx index 4dff80f7..c698a0db 100644 --- a/frontend/src/screens/apps/ShowApp.tsx +++ b/frontend/src/screens/apps/ShowApp.tsx @@ -238,7 +238,7 @@ function ShowApp() { initialPermissions={permissions} onPermissionsChange={setPermissions} budgetUsage={app.budgetUsage} - isEditing={editMode} + canEditPermissions={editMode} />