diff --git a/frontend/src/components/connections/AppLinksCard.tsx b/frontend/src/components/connections/AppLinksCard.tsx index 10eaf396..b79062ed 100644 --- a/frontend/src/components/connections/AppLinksCard.tsx +++ b/frontend/src/components/connections/AppLinksCard.tsx @@ -13,15 +13,66 @@ import { } from "src/components/ui/card"; import { ExternalLinkButton } from "src/components/ui/custom/external-link-button"; +function hasAppLinks(appStoreApp: AppStoreApp) { + return !!( + appStoreApp.appleLink || + appStoreApp.playLink || + appStoreApp.zapStoreLink || + appStoreApp.chromeLink || + appStoreApp.firefoxLink || + appStoreApp.webLink + ); +} + +export function AppLinks({ appStoreApp }: { appStoreApp: AppStoreApp }) { + if (!hasAppLinks(appStoreApp)) { + return null; + } + + return ( +
+ {appStoreApp.webLink && ( + + + Website + + )} + {appStoreApp.playLink && ( + + + Play Store + + )} + {appStoreApp.appleLink && ( + + + App Store + + )} + {appStoreApp.zapStoreLink && ( + + + Zapstore + + )} + {appStoreApp.chromeLink && ( + + + Chrome Web Store + + )} + {appStoreApp.firefoxLink && ( + + + Firefox Add-Ons + + )} +
+ ); +} + export function AppLinksCard({ appStoreApp }: { appStoreApp: AppStoreApp }) { - if ( - !appStoreApp.appleLink && - !appStoreApp.playLink && - !appStoreApp.zapStoreLink && - !appStoreApp.chromeLink && - !appStoreApp.firefoxLink && - !appStoreApp.webLink - ) { + if (!hasAppLinks(appStoreApp)) { return null; } @@ -30,43 +81,8 @@ export function AppLinksCard({ appStoreApp }: { appStoreApp: AppStoreApp }) { Links - - {appStoreApp.webLink && ( - - - Website - - )} - {appStoreApp.playLink && ( - - - Play Store - - )} - {appStoreApp.appleLink && ( - - - App Store - - )} - {appStoreApp.zapStoreLink && ( - - - Zapstore - - )} - {appStoreApp.chromeLink && ( - - - Chrome Web Store - - )} - {appStoreApp.firefoxLink && ( - - - Firefox Add-Ons - - )} + + ); diff --git a/frontend/src/components/connections/AppUsage.tsx b/frontend/src/components/connections/AppUsage.tsx index 0cd745d5..64087be7 100644 --- a/frontend/src/components/connections/AppUsage.tsx +++ b/frontend/src/components/connections/AppUsage.tsx @@ -72,31 +72,34 @@ export function AppUsage({ app }: { app: App }) { deletingLightningAddress, } = useDeleteLightningAddress(app.id); + const isSubwallet = + app.metadata?.app_store_app_id === SUBWALLET_APPSTORE_APP_ID; + return ( <> - {app.isolated && ( -
- - - Isolated Balance - - -
-
-

- -

- -
-
+ + + Usage + + +
+ {app.isolated && ( +
+

+ Isolated Balance +

+

+ +

+ +
{app.balanceMsat > 0 && (
- - - {app.metadata?.app_store_app_id === SUBWALLET_APPSTORE_APP_ID && ( - - - Lightning Address - - {!app.metadata.lud16 && ( - -

- Create a lightning address for this particular app - connection -

-
- )} -
- - {!app.metadata.lud16 && ( -
- - setIntendedLightningAddress(e.target.value) - } - required - autoComplete="off" - endAdornment={ - - @getalby.com - - } - className="flex-1" - /> - {!albyMe?.subscription.plan_code ? ( - - - - ) : ( - - createLightningAddress(intendedLightningAddress) - } - > - Create - - )} -
- )} - {app.metadata.lud16 && ( -
-

- {app.metadata.lud16} -

-
- - - - Remove - -
-
- )} -
-
- )} -
- )} - -
- - - Total Spent - - -

- -

- -
-
- - - Total Received - - -

- -

- -
-
-
- - {app.maxAmountSat > 0 && ( - - - Budget - - -
-
-

- Left in budget -

-

- -

- -
-
-

- Budget renewal -

-

- - {app.budgetRenewal !== "never" && ( - <> / {getBudgetRenewalLabel(app.budgetRenewal)} - )} -

- -
+ )} +
+

+ Total Spent +

+

+ +

+
- +
+

+ Total Received +

+

+ +

+ +
+
+ + {app.maxAmountSat > 0 && ( +
+
+
+

+ Left in budget +

+

+ +

+ +
+
+

+ Budget renewal +

+

+ + {app.budgetRenewal !== "never" && ( + <> / {getBudgetRenewalLabel(app.budgetRenewal)} + )} +

+ +
+
+ +
+ )} +
+
+ + {app.isolated && isSubwallet && ( + + + Lightning Address + + {!app.metadata?.lud16 && ( + + Create a lightning address for this particular app connection + + )} + + + {!app.metadata?.lud16 && ( +
+ setIntendedLightningAddress(e.target.value)} + required + autoComplete="off" + endAdornment={ + + @getalby.com + + } + className="flex-1" + /> + {!albyMe?.subscription.plan_code ? ( + + + + ) : ( + + createLightningAddress(intendedLightningAddress) + } + > + Create + + )} +
+ )} + {app.metadata?.lud16 && ( +
+

+ {app.metadata.lud16} +

+
+ + + + Remove + +
+
+ )}
)} diff --git a/frontend/src/screens/apps/AppDetails.tsx b/frontend/src/screens/apps/AppDetails.tsx index 22ebad81..38f2693e 100644 --- a/frontend/src/screens/apps/AppDetails.tsx +++ b/frontend/src/screens/apps/AppDetails.tsx @@ -4,6 +4,8 @@ import { Link, useLocation, useParams } from "react-router"; import { App, AppPermissions, + BudgetRenewalType, + Scope, UpdateAppRequest, WalletCapabilities, } from "src/types"; @@ -24,8 +26,8 @@ import { 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 { AppLinks } from "src/components/connections/AppLinksCard"; +import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; import { AppTransactionList } from "src/components/connections/AppTransactionList"; import { AppUsage } from "src/components/connections/AppUsage"; import { ConnectionDetailsModal } from "src/components/connections/ConnectionDetailsModal"; @@ -69,6 +71,95 @@ import { useAppsForAppStoreApp } from "src/hooks/useApps"; import { useCapabilities } from "src/hooks/useCapabilities"; import { cn, getAppDisplayName } from "src/lib/utils"; +function formatList(items: string[]): string { + if (items.length === 1) { + return items[0]; + } + if (items.length === 2) { + return `${items[0]} and ${items[1]}`; + } + return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`; +} + +const budgetPeriodLabels: Record = { + daily: "day", + weekly: "week", + monthly: "month", + yearly: "year", + never: "", + "": "", +}; + +function getSecondaryAbilities(scopes: Scope[]): string[] { + const abilities: string[] = []; + if (scopes.includes("make_invoice")) { + abilities.push("receive payments"); + } + if ( + scopes.includes("get_balance") || + scopes.includes("get_info") || + scopes.includes("list_transactions") || + scopes.includes("lookup_invoice") + ) { + abilities.push("read your balance and activity"); + } + if (scopes.includes("sign_message")) { + abilities.push("sign messages"); + } + return abilities; +} + +function PermissionsSummary({ + scopes, + maxAmountSat, + budgetRenewal, +}: { + scopes: Scope[]; + maxAmountSat: number; + budgetRenewal: BudgetRenewalType; +}) { + if (scopes.includes("superuser")) { + return ( +

+ This app has full access and can create other connections to your + wallet. +

+ ); + } + + const canSpend = scopes.includes("pay_invoice"); + const secondary = getSecondaryAbilities(scopes); + + if (!canSpend && !secondary.length) { + return

This app has no access to your wallet.

; + } + + if (!canSpend) { + return

This app can {formatList(secondary)}.

; + } + + return ( +

+ This app can send payments + {maxAmountSat > 0 ? ( + <> + {" "} + up to{" "} + + + + {budgetRenewal !== "never" && ( + <> per {budgetPeriodLabels[budgetRenewal]} + )} + + ) : ( + <> with no spending limit set + )} + .{!!secondary.length && <> It can also {formatList(secondary)}.} +

+ ); +} + function AppDetails() { const { id } = useParams() as { id: string }; const { data: app, mutate: refetchApp, error } = useApp(parseInt(id)); @@ -105,6 +196,8 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) { React.useState(false); const [showDisconnectAppDialog, setShowDisconnectAppDialog] = React.useState(false); + const [showPermissionDetails, setShowPermissionDetails] = + React.useState(false); const { data: albyMe } = useAlbyMe(); @@ -190,6 +283,46 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) { (!app.scopes.includes("pay_invoice") && permissions.scopes.includes("pay_invoice")); + const permissionsSection = ( +
+
+

Permissions

+ +
+ {showPermissionDetails ? ( + + ) : ( +
+ + {app.expiresAt && ( +

Expires {new Date(app.expiresAt).toLocaleDateString()}

+ )} +
+ )} +
+ ); + return ( <>
@@ -390,17 +523,6 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) { } description={""} /> - {!isEditingPermissions && ( - <> - {appStoreApp && ( -
- - -
- )} - - - )} {isEditingPermissions ? (
) : ( - - - -
- Permissions -
-
-
- - - -
+
+
+ + {!appStoreApp && ( + + + {permissionsSection} + + + )} + +
+ {appStoreApp && ( + + + About the App + + +

+ {appStoreApp.extendedDescription} +

+ +
{permissionsSection}
+
+
+ )} +
)} {!isEditingPermissions && ( <> @@ -485,7 +627,6 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) { onClose={() => setShowDisconnectAppDialog(false)} /> )} - )}