import React from "react"; import { Link, useLocation, useParams } from "react-router"; import { App, AppPermissions, UpdateAppRequest, WalletCapabilities, } from "src/types"; import { handleRequestError } from "src/utils/handleRequestError"; import { request } from "src/utils/request"; // build the project for this to appear import { CheckCircleIcon, ChevronDownIcon, EllipsisIcon, InfoIcon, PlusIcon, SquarePenIcon, SquareStackIcon, UnplugIcon, } from "lucide-react"; import { toast } from "sonner"; import AppAvatar from "src/components/AppAvatar"; import AppHeader from "src/components/AppHeader"; import { AboutAppCard } from "src/components/connections/AboutAppCard"; import { AppLinksCard } from "src/components/connections/AppLinksCard"; import { AppTransactionList } from "src/components/connections/AppTransactionList"; import { AppUsage } from "src/components/connections/AppUsage"; import { ConnectionDetailsModal } from "src/components/connections/ConnectionDetailsModal"; import { DisconnectApp } from "src/components/connections/DisconnectApp"; import { getAppStoreApp } from "src/components/connections/SuggestedAppData"; import Loading from "src/components/Loading"; import Permissions from "src/components/Permissions"; import ResponsiveButton from "src/components/ResponsiveButton"; import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogTitle, } from "src/components/ui/alert-dialog"; import { Badge } from "src/components/ui/badge"; import { Button } from "src/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle, } from "src/components/ui/card"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "src/components/ui/dropdown-menu"; import { Input } from "src/components/ui/input"; import { ALBY_ACCOUNT_APP_NAME, SUBWALLET_APPSTORE_APP_ID, } from "src/constants"; import { useAlbyMe } from "src/hooks/useAlbyMe"; import { useApp } from "src/hooks/useApp"; import { useAppsForAppStoreApp } from "src/hooks/useApps"; import { useCapabilities } from "src/hooks/useCapabilities"; import { cn, getAppDisplayName } from "src/lib/utils"; function AppDetails() { const { id } = useParams() as { id: string }; const { data: app, mutate: refetchApp, error } = useApp(parseInt(id)); const { data: capabilities } = useCapabilities(); if (error) { return

{error.message}

; } if (!app || !capabilities) { return ; } return ( ); } type AppInternalProps = { app: App; capabilities: WalletCapabilities; refetchApp: () => void; }; function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) { const location = useLocation(); const [isEditingPermissions, setIsEditingPermissions] = React.useState(false); const [showConfirmDialog, setShowConfirmDialog] = React.useState(false); const [showConnectionDetails, setShowConnectionDetails] = React.useState(false); const [showDisconnectAppDialog, setShowDisconnectAppDialog] = React.useState(false); const { data: albyMe } = useAlbyMe(); React.useEffect(() => { const queryParams = new URLSearchParams(location.search); const editMode = queryParams.has("edit"); setIsEditingPermissions(editMode); }, [location.search]); const [name, setName] = React.useState(app.name); const [permissions, setPermissions] = React.useState({ scopes: app.scopes, maxAmountSat: app.maxAmountSat, budgetRenewal: app.budgetRenewal, expiresAt: app.expiresAt ? new Date(app.expiresAt) : undefined, isolated: app.isolated, }); const [savedPermissions, setSavedPermissions] = React.useState(permissions); const handleSave = async () => { try { const updateAppRequest: UpdateAppRequest = { name, scopes: Array.from(permissions.scopes), budgetRenewal: permissions.budgetRenewal, expiresAt: permissions.expiresAt?.toISOString(), updateExpiresAt: true, maxAmountSat: permissions.maxAmountSat, isolated: permissions.isolated, }; await request(`/api/apps/${app.appPubkey}`, { method: "PATCH", headers: { "Content-Type": "application/json", }, body: JSON.stringify(updateAppRequest), }); refetchApp(); setIsEditingPermissions(false); setSavedPermissions(permissions); toast("Successfully updated connection"); } catch (error) { handleRequestError("Failed to update connection", error); } }; const handleConvertToSubwallet = async () => { try { // Only send the metadata since that's the only thing changing const updateAppRequest: UpdateAppRequest = { metadata: { ...app.metadata, app_store_app_id: SUBWALLET_APPSTORE_APP_ID, }, }; await request(`/api/apps/${app.appPubkey}`, { method: "PATCH", headers: { "Content-Type": "application/json", }, body: JSON.stringify(updateAppRequest), }); refetchApp(); toast("Successfully converted to sub-wallet", { description: "This isolated app is now a sub-wallet.", }); } catch (error) { handleRequestError("Failed to convert to sub-wallet", error); } }; const appName = getAppDisplayName(app.name); const appStoreApp = getAppStoreApp(app); const connectedApps = useAppsForAppStoreApp(appStoreApp); const needsConfirmation = (app.isolated && !permissions.isolated) || (!app.scopes.includes("pay_invoice") && permissions.scopes.includes("pay_invoice")); return ( <>

{appName}

{(connectedApps?.length || 0) > 1 ? (
{`${connectedApps?.length} Connections`}{" "}
{connectedApps?.map((connectedApp) => ( {connectedApp.name} ))}
) : ( <> Connected )}
} contentRight={
{!isEditingPermissions && ( <> {app.isolated && !app.metadata?.app_store_app_id && albyMe?.subscription.plan_code && ( Convert to Sub-wallet )} {appStoreApp && ( Add Another Connection )}
setShowConnectionDetails(true)} > Connection Details
setShowDisconnectAppDialog(true)} > Disconnect{" "} {appName}
setIsEditingPermissions(true)} icon={SquarePenIcon} text="Edit Connection" /> )} {isEditingPermissions && ( <> {isEditingPermissions && (
{needsConfirmation ? ( Confirm Update App
{app.isolated && !permissions.isolated ? (

Are you sure you wish to remove the{" "} isolated {" "} status from this connection?

) : (

Are you sure you wish to give this connection{" "} pay permissions ?

)}

⚠️ Warning: This applies to all apps that have this connection secret. Only change this if you know it is safe to do so, otherwise you could potentially lose all funds {!!permissions.maxAmountSat && " up to the specified budget"} {permissions.isolated && " that are deposited into this isolated app"} .

Cancel
) : ( )}
)} )}
} description={""} /> {!isEditingPermissions && ( <> {appStoreApp && (
)} )} {isEditingPermissions ? (
{ e.preventDefault(); if (needsConfirmation) { setShowConfirmDialog(true); return; } handleSave(); }} > {app.name !== ALBY_ACCOUNT_APP_NAME && ( App Name
setName(e.target.value)} required autoComplete="off" />
)}
Permissions
) : (
Permissions
)} {!isEditingPermissions && ( <> {showConnectionDetails && ( setShowConnectionDetails(false)} /> )} {showDisconnectAppDialog && ( setShowDisconnectAppDialog(false)} /> )} )}
); } export default AppDetails;