Skip to content
This repository has been archived by the owner on Sep 2, 2024. It is now read-only.

chore: merge app connection flows #377

Merged
merged 5 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -79,8 +77,6 @@ function App() {
</Route>
<Route path="appstore" element={<DefaultRedirect />}>
<Route index element={<AppStore />} />
<Route path=":id" element={<AppDetail />} />
<Route path=":id/connect" element={<AppConnect />} />
</Route>
<Route path="apps" element={<DefaultRedirect />}>
<Route path="new" element={<NewApp />} />
Expand Down
41 changes: 23 additions & 18 deletions frontend/src/components/Permissions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<PermissionsProps> = ({
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);
Expand All @@ -54,7 +54,7 @@ const Permissions: React.FC<PermissionsProps> = ({
};

const handleRequestMethodChange = (requestMethod: PermissionType) => {
if (!isEditing) {
if (!canEditPermissions) {
return;
}

Expand Down Expand Up @@ -109,7 +109,7 @@ const Permissions: React.FC<PermissionsProps> = ({
className={cn(
"w-full",
rm == "pay_invoice" ? "order-last" : "",
!isEditing && !permissions.requestMethods.has(rm)
!canEditPermissions && !permissions.requestMethods.has(rm)
? "hidden"
: ""
)}
Expand All @@ -119,19 +119,22 @@ const Permissions: React.FC<PermissionsProps> = ({
<RequestMethodIcon
className={cn(
"text-muted-foreground w-4 mr-3",
isEditing ? "hidden" : ""
canEditPermissions ? "hidden" : ""
)}
/>
)}
<Checkbox
id={rm}
className={cn("mr-2", !isEditing ? "hidden" : "")}
className={cn(
"mr-2",
!canEditPermissions ? "hidden" : ""
)}
onCheckedChange={() => handleRequestMethodChange(rm)}
checked={permissions.requestMethods.has(rm)}
/>
<Label
htmlFor={rm}
className={`${isEditing && "cursor-pointer"}`}
className={`${canEditPermissions && "cursor-pointer"}`}
>
{nip47PermissionDescriptions[rm]}
</Label>
Expand All @@ -141,23 +144,23 @@ const Permissions: React.FC<PermissionsProps> = ({
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 ? (
<>
<div className="flex flex-row gap-2 items-center text-muted-foreground mb-3 text-sm capitalize">
<p> Budget Renewal:</p>
{!isEditing ? (
{!canEditPermissions ? (
permissions.budgetRenewal
) : (
<Select
value={permissions.budgetRenewal}
onValueChange={handleBudgetRenewalChange}
disabled={!isEditing}
disabled={!canEditPermissions}
>
<SelectTrigger className="w-[150px]">
<SelectValue
Expand Down Expand Up @@ -209,7 +212,7 @@ const Permissions: React.FC<PermissionsProps> = ({
})}
</div>
</>
) : isNew ? (
) : isNewConnection ? (
<>
<p className="text-muted-foreground text-sm">
<span className="capitalize">
Expand Down Expand Up @@ -254,7 +257,9 @@ const Permissions: React.FC<PermissionsProps> = ({
</ul>
</div>

{(isNew ? !permissions.expiresAt || days : isEditing) ? (
{(
isNewConnection ? !permissions.expiresAt || days : canEditPermissions
) ? (
<>
{!expireOptions && (
<Button
Expand All @@ -270,7 +275,7 @@ const Permissions: React.FC<PermissionsProps> = ({
{expireOptions && (
<div className="mt-5">
<p className="font-medium text-sm mb-2">Connection expiration</p>
{!isNew && (
{!isNewConnection && (
<p className="mb-2 text-muted-foreground text-sm">
Expires:{" "}
{permissions.expiresAt &&
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/SuggestedApps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { SuggestedApp, suggestedApps } from "./SuggestedAppData";

function SuggestedAppCard({ id, title, description, logo }: SuggestedApp) {
return (
<Link to={`/appstore/${id}`}>
<Link to={`/apps/new?app=${id}`}>
<Card>
<CardContent className="pt-6">
<div className="flex gap-3 items-center">
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/ui/carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ const CarouselDots = React.forwardRef<
React.HTMLAttributes<HTMLDivElement>
>((props, ref) => {
const { api } = useCarousel();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, setUpdateState] = React.useState(false);
const toggleUpdateState = React.useCallback(
() => setUpdateState((prevState) => !prevState),
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/ui/loading-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,5 @@ const LoadingButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
);
LoadingButton.displayName = "LoadingButton";

// eslint-disable-next-line react-refresh/only-export-components
export { LoadingButton, buttonVariants };
4 changes: 3 additions & 1 deletion frontend/src/components/ui/use-toast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ function toast({ ...props }: Toast) {
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
if (!open) {
dismiss();
}
},
},
});
Expand Down
164 changes: 109 additions & 55 deletions frontend/src/screens/apps/AppCreated.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,66 @@
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(() => {
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
im-adithya marked this conversation as resolved.
Show resolved Hide resolved
const nwcEvent = new CustomEvent("nwc:success", { detail: {} });
Expand All @@ -36,58 +76,72 @@ export default function AppCreated() {
"*"
);
}
}, []);
}, [appstoreApp]);

if (!createAppResponse) {
return <Navigate to="/apps/new" />;
}

const pairingUri = createAppResponse.pairingUri;

const copy = () => {
copyToClipboard(pairingUri);
toast({ title: "Copied to clipboard." });
};

return (
<div className="w-full max-w-screen-sm mx-auto mt-6 md:px-4 ph-no-capture">
<h2 className="font-bold text-2xl font-headline mb-2 text-center">
🚀 Almost there!
</h2>
<div className="font-medium text-muted-foreground text-center mb-6">
Complete the last step of the setup by pasting or scanning your
connection's pairing secret in the desired app to finalise the
connection.
</div>

<div className="flex flex-col items-center gap-3">
<Button size="lg" onClick={copy}>
<CopyIcon className="w-4 h-4 mr-2" />
Copy pairing secret
</Button>
<Dialog>
<DialogTrigger asChild>
<Button variant="secondary">
<QrCode className="w-4 h-4 mr-2" />
QR-Code
</Button>
</DialogTrigger>

<DialogContent>
<DialogHeader>
<DialogTitle>Scan QR Code</DialogTitle>
<DialogDescription>
Open the app you want to pair and scan this QR code to connect.
</DialogDescription>
</DialogHeader>
<div className="flex flex-row justify-center p-3">
<a href={pairingUri} target="_blank">
<QRCode value={pairingUri} />
</a>
<>
<AppHeader
title={`Connect to ${createAppResponse.name}`}
description="Configure wallet permissions for the app and follow instructions to finalise the connection"
/>
<div className="flex flex-col gap-3 ph-no-capture">
<div>
<p>
1. Open{" "}
{appstoreApp ? (
<ExternalLink
className="font-semibold underline"
to={appstoreApp.to}
>
{appstoreApp.title}
</ExternalLink>
) : (
"the app you wish to connect"
)}{" "}
and look for a way to attach a wallet (most apps provide this option
in settings)
</p>
<p>2. Scan or paste the connection secret</p>
</div>
<Card className="max-w-sm">
<CardHeader>
<CardTitle className="text-center">Connection Secret</CardTitle>
</CardHeader>
<CardContent className="flex flex-col items-center gap-5">
<div className="flex flex-row items-center gap-2 text-sm">
<Loading className="w-4 h-4" />
<p>Waiting for app to connect</p>
</div>
{timeout && (
<div className="text-sm flex flex-col gap-2 items-center text-center">
Connecting is taking longer than usual.
<Link to={`/apps/${app?.nostrPubkey}`}>
<Button variant="secondary">Continue anyway</Button>
</Link>
</div>
)}
<a href={pairingUri} target="_blank" className="relative">
<QRCode value={pairingUri} className="w-full" />
{appstoreApp && (
<img
src={appstoreApp.logo}
className="absolute w-12 h-12 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-muted p-1 rounded-xl"
/>
)}
</a>
<div>
<Button onClick={copy} variant="outline">
<CopyIcon className="w-4 h-4 mr-2" />
Copy pairing secret
</Button>
</div>
</DialogContent>
</Dialog>
</CardContent>
</Card>
</div>
</div>
</>
);
}
Loading
Loading