mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
feat: redesign app details page
- Two-column dashboard layout (Usage/Permissions/Transactions main, About sidebar) - Merge balance/spent/received and budget into a single Usage card - Replace permissions pill wall with a plain-language risk-first summary, with a Show details toggle for the full scope list - Tighten card spacing
This commit is contained in:
parent
81b1b2f695
commit
4f270dac83
3 changed files with 390 additions and 248 deletions
|
|
@ -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 (
|
||||
<div className="flex flex-row flex-wrap gap-2">
|
||||
{appStoreApp.webLink && (
|
||||
<ExternalLinkButton to={appStoreApp.webLink} variant="outline">
|
||||
<GlobeIcon />
|
||||
Website
|
||||
</ExternalLinkButton>
|
||||
)}
|
||||
{appStoreApp.playLink && (
|
||||
<ExternalLinkButton to={appStoreApp.playLink} variant="outline">
|
||||
<PlayStoreIcon />
|
||||
Play Store
|
||||
</ExternalLinkButton>
|
||||
)}
|
||||
{appStoreApp.appleLink && (
|
||||
<ExternalLinkButton to={appStoreApp.appleLink} variant="outline">
|
||||
<AppleIcon />
|
||||
App Store
|
||||
</ExternalLinkButton>
|
||||
)}
|
||||
{appStoreApp.zapStoreLink && (
|
||||
<ExternalLinkButton to={appStoreApp.zapStoreLink} variant="outline">
|
||||
<ZapStoreIcon />
|
||||
Zapstore
|
||||
</ExternalLinkButton>
|
||||
)}
|
||||
{appStoreApp.chromeLink && (
|
||||
<ExternalLinkButton to={appStoreApp.chromeLink} variant="outline">
|
||||
<ChromeIcon />
|
||||
Chrome Web Store
|
||||
</ExternalLinkButton>
|
||||
)}
|
||||
{appStoreApp.firefoxLink && (
|
||||
<ExternalLinkButton to={appStoreApp.firefoxLink} variant="outline">
|
||||
<FirefoxIcon />
|
||||
Firefox Add-Ons
|
||||
</ExternalLinkButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 }) {
|
|||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Links</CardTitle>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex flex-row flex-wrap gap-2">
|
||||
{appStoreApp.webLink && (
|
||||
<ExternalLinkButton to={appStoreApp.webLink} variant="outline">
|
||||
<GlobeIcon />
|
||||
Website
|
||||
</ExternalLinkButton>
|
||||
)}
|
||||
{appStoreApp.playLink && (
|
||||
<ExternalLinkButton to={appStoreApp.playLink} variant="outline">
|
||||
<PlayStoreIcon />
|
||||
Play Store
|
||||
</ExternalLinkButton>
|
||||
)}
|
||||
{appStoreApp.appleLink && (
|
||||
<ExternalLinkButton to={appStoreApp.appleLink} variant="outline">
|
||||
<AppleIcon />
|
||||
App Store
|
||||
</ExternalLinkButton>
|
||||
)}
|
||||
{appStoreApp.zapStoreLink && (
|
||||
<ExternalLinkButton to={appStoreApp.zapStoreLink} variant="outline">
|
||||
<ZapStoreIcon />
|
||||
Zapstore
|
||||
</ExternalLinkButton>
|
||||
)}
|
||||
{appStoreApp.chromeLink && (
|
||||
<ExternalLinkButton to={appStoreApp.chromeLink} variant="outline">
|
||||
<ChromeIcon />
|
||||
Chrome Web Store
|
||||
</ExternalLinkButton>
|
||||
)}
|
||||
{appStoreApp.firefoxLink && (
|
||||
<ExternalLinkButton to={appStoreApp.firefoxLink} variant="outline">
|
||||
<FirefoxIcon />
|
||||
Firefox Add-Ons
|
||||
</ExternalLinkButton>
|
||||
)}
|
||||
<CardFooter>
|
||||
<AppLinks appStoreApp={appStoreApp} />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
<div
|
||||
className={cn(
|
||||
"grid grid-cols-1 gap-3",
|
||||
app.metadata?.app_store_app_id === SUBWALLET_APPSTORE_APP_ID &&
|
||||
"lg:grid-cols-2"
|
||||
)}
|
||||
>
|
||||
<Card className="justify-between">
|
||||
<CardHeader>
|
||||
<CardTitle>Isolated Balance</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<p className="font-medium text-2xl">
|
||||
<FormattedBitcoinAmount amountMsat={app.balanceMsat} />
|
||||
</p>
|
||||
<FormattedFiatAmount
|
||||
amountSat={Math.floor(app.balanceMsat / 1000)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Card className="slashed-zero gap-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Usage</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-5">
|
||||
<div
|
||||
className={cn(
|
||||
"grid grid-cols-1 gap-6 sm:gap-4",
|
||||
app.isolated ? "sm:grid-cols-3" : "sm:grid-cols-2"
|
||||
)}
|
||||
>
|
||||
{app.isolated && (
|
||||
<div className="flex flex-col">
|
||||
<p className="text-xs text-secondary-foreground font-medium">
|
||||
Isolated Balance
|
||||
</p>
|
||||
<p className="text-xl font-medium">
|
||||
<FormattedBitcoinAmount amountMsat={app.balanceMsat} />
|
||||
</p>
|
||||
<FormattedFiatAmount
|
||||
amountSat={Math.floor(app.balanceMsat / 1000)}
|
||||
/>
|
||||
<div className="flex gap-2 items-center mt-3">
|
||||
{app.balanceMsat > 0 && (
|
||||
<IsolatedAppDrawDownDialog appId={app.id}>
|
||||
<Button size="sm" variant="outline">
|
||||
|
|
@ -113,156 +116,138 @@ export function AppUsage({ app }: { app: App }) {
|
|||
</IsolatedAppTopupDialog>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{app.metadata?.app_store_app_id === SUBWALLET_APPSTORE_APP_ID && (
|
||||
<Card className="justify-between">
|
||||
<CardHeader>
|
||||
<CardTitle>Lightning Address</CardTitle>
|
||||
|
||||
{!app.metadata.lud16 && (
|
||||
<CardDescription>
|
||||
<p className="text-muted-foreground">
|
||||
Create a lightning address for this particular app
|
||||
connection
|
||||
</p>
|
||||
</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!app.metadata.lud16 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<InputWithAdornment
|
||||
type="text"
|
||||
value={intendedLightningAddress}
|
||||
onChange={(e) =>
|
||||
setIntendedLightningAddress(e.target.value)
|
||||
}
|
||||
required
|
||||
autoComplete="off"
|
||||
endAdornment={
|
||||
<span className="mr-1 text-muted-foreground text-xs">
|
||||
@getalby.com
|
||||
</span>
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
{!albyMe?.subscription.plan_code ? (
|
||||
<UpgradeDialog>
|
||||
<Button className="shrink-0" variant="secondary">
|
||||
Create
|
||||
</Button>
|
||||
</UpgradeDialog>
|
||||
) : (
|
||||
<LoadingButton
|
||||
className="shrink-0"
|
||||
variant="secondary"
|
||||
loading={creatingLightningAddress}
|
||||
onClick={() =>
|
||||
createLightningAddress(intendedLightningAddress)
|
||||
}
|
||||
>
|
||||
Create
|
||||
</LoadingButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{app.metadata.lud16 && (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
||||
<p className="font-semibold break-all min-w-0 flex-1">
|
||||
{app.metadata.lud16}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
copyToClipboard(app.metadata?.lud16 || "")
|
||||
}
|
||||
variant="outline"
|
||||
>
|
||||
<CopyIcon />
|
||||
Copy
|
||||
</Button>
|
||||
<LoadingButton
|
||||
size="sm"
|
||||
variant="outline"
|
||||
loading={deletingLightningAddress}
|
||||
onClick={deleteSubwalletLightningAddress}
|
||||
>
|
||||
<Trash2Icon />
|
||||
Remove
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Total Spent</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="font-medium text-2xl">
|
||||
<FormattedBitcoinAmount amountMsat={totalSpentSat * 1000} />
|
||||
</p>
|
||||
<FormattedFiatAmount amountSat={totalSpentSat} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Total Received</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="font-medium text-2xl">
|
||||
<FormattedBitcoinAmount amountMsat={totalReceivedSat * 1000} />
|
||||
</p>
|
||||
<FormattedFiatAmount amountSat={totalReceivedSat} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{app.maxAmountSat > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Budget</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-row justify-between mb-2">
|
||||
<div>
|
||||
<p className="text-xs text-secondary-foreground font-medium">
|
||||
Left in budget
|
||||
</p>
|
||||
<p className="text-xl font-medium">
|
||||
<FormattedBitcoinAmount
|
||||
amountMsat={app.maxAmountMsat - app.budgetUsageMsat}
|
||||
/>
|
||||
</p>
|
||||
<FormattedFiatAmount
|
||||
amountSat={app.maxAmountSat - app.budgetUsageSat}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-secondary-foreground font-medium">
|
||||
Budget renewal
|
||||
</p>
|
||||
<p className="text-xl font-medium">
|
||||
<FormattedBitcoinAmount amountMsat={app.maxAmountMsat} />
|
||||
{app.budgetRenewal !== "never" && (
|
||||
<> / {getBudgetRenewalLabel(app.budgetRenewal)}</>
|
||||
)}
|
||||
</p>
|
||||
<FormattedFiatAmount amountSat={app.maxAmountSat} />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<p className="text-xs text-secondary-foreground font-medium">
|
||||
Total Spent
|
||||
</p>
|
||||
<p className="text-xl font-medium">
|
||||
<FormattedBitcoinAmount amountMsat={totalSpentSat * 1000} />
|
||||
</p>
|
||||
<FormattedFiatAmount amountSat={totalSpentSat} />
|
||||
</div>
|
||||
<Progress
|
||||
className="h-4"
|
||||
value={100 - (app.budgetUsageSat * 100) / app.maxAmountSat}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-xs text-secondary-foreground font-medium">
|
||||
Total Received
|
||||
</p>
|
||||
<p className="text-xl font-medium">
|
||||
<FormattedBitcoinAmount amountMsat={totalReceivedSat * 1000} />
|
||||
</p>
|
||||
<FormattedFiatAmount amountSat={totalReceivedSat} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{app.maxAmountSat > 0 && (
|
||||
<div className="flex flex-col gap-2 border-t pt-5">
|
||||
<div className="flex flex-row justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-secondary-foreground font-medium">
|
||||
Left in budget
|
||||
</p>
|
||||
<p className="text-xl font-medium">
|
||||
<FormattedBitcoinAmount
|
||||
amountMsat={app.maxAmountMsat - app.budgetUsageMsat}
|
||||
/>
|
||||
</p>
|
||||
<FormattedFiatAmount
|
||||
amountSat={app.maxAmountSat - app.budgetUsageSat}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-secondary-foreground font-medium">
|
||||
Budget renewal
|
||||
</p>
|
||||
<p className="text-xl font-medium">
|
||||
<FormattedBitcoinAmount amountMsat={app.maxAmountMsat} />
|
||||
{app.budgetRenewal !== "never" && (
|
||||
<> / {getBudgetRenewalLabel(app.budgetRenewal)}</>
|
||||
)}
|
||||
</p>
|
||||
<FormattedFiatAmount amountSat={app.maxAmountSat} />
|
||||
</div>
|
||||
</div>
|
||||
<Progress
|
||||
className="h-2 mt-1"
|
||||
value={100 - (app.budgetUsageSat * 100) / app.maxAmountSat}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{app.isolated && isSubwallet && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Lightning Address</CardTitle>
|
||||
|
||||
{!app.metadata?.lud16 && (
|
||||
<CardDescription>
|
||||
Create a lightning address for this particular app connection
|
||||
</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!app.metadata?.lud16 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<InputWithAdornment
|
||||
type="text"
|
||||
value={intendedLightningAddress}
|
||||
onChange={(e) => setIntendedLightningAddress(e.target.value)}
|
||||
required
|
||||
autoComplete="off"
|
||||
endAdornment={
|
||||
<span className="mr-1 text-muted-foreground text-xs">
|
||||
@getalby.com
|
||||
</span>
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
{!albyMe?.subscription.plan_code ? (
|
||||
<UpgradeDialog>
|
||||
<Button className="shrink-0" variant="secondary">
|
||||
Create
|
||||
</Button>
|
||||
</UpgradeDialog>
|
||||
) : (
|
||||
<LoadingButton
|
||||
className="shrink-0"
|
||||
variant="secondary"
|
||||
loading={creatingLightningAddress}
|
||||
onClick={() =>
|
||||
createLightningAddress(intendedLightningAddress)
|
||||
}
|
||||
>
|
||||
Create
|
||||
</LoadingButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{app.metadata?.lud16 && (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
||||
<p className="font-semibold break-all min-w-0 flex-1">
|
||||
{app.metadata.lud16}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => copyToClipboard(app.metadata?.lud16 || "")}
|
||||
variant="outline"
|
||||
>
|
||||
<CopyIcon />
|
||||
Copy
|
||||
</Button>
|
||||
<LoadingButton
|
||||
size="sm"
|
||||
variant="outline"
|
||||
loading={deletingLightningAddress}
|
||||
onClick={deleteSubwalletLightningAddress}
|
||||
>
|
||||
<Trash2Icon />
|
||||
Remove
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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<BudgetRenewalType, string> = {
|
||||
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 (
|
||||
<p>
|
||||
This app has full access and can create other connections to your
|
||||
wallet.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
const canSpend = scopes.includes("pay_invoice");
|
||||
const secondary = getSecondaryAbilities(scopes);
|
||||
|
||||
if (!canSpend && !secondary.length) {
|
||||
return <p>This app has no access to your wallet.</p>;
|
||||
}
|
||||
|
||||
if (!canSpend) {
|
||||
return <p>This app can {formatList(secondary)}.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<p>
|
||||
This app can send payments
|
||||
{maxAmountSat > 0 ? (
|
||||
<>
|
||||
{" "}
|
||||
up to{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
<FormattedBitcoinAmount amountMsat={maxAmountSat * 1000} />
|
||||
</span>
|
||||
{budgetRenewal !== "never" && (
|
||||
<> per {budgetPeriodLabels[budgetRenewal]}</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<> with no spending limit set</>
|
||||
)}
|
||||
.{!!secondary.length && <> It can also {formatList(secondary)}.</>}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
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 = (
|
||||
<div>
|
||||
<div className="flex flex-row justify-between items-center mb-2">
|
||||
<p className="font-semibold">Permissions</p>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
|
||||
onClick={() => setShowPermissionDetails((current) => !current)}
|
||||
>
|
||||
{showPermissionDetails ? "Hide details" : "Show details"}
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
"size-4 transition-transform",
|
||||
showPermissionDetails && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{showPermissionDetails ? (
|
||||
<Permissions
|
||||
capabilities={capabilities}
|
||||
permissions={savedPermissions}
|
||||
setPermissions={setPermissions}
|
||||
readOnly
|
||||
/>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground space-y-1">
|
||||
<PermissionsSummary
|
||||
scopes={app.scopes}
|
||||
maxAmountSat={app.maxAmountSat}
|
||||
budgetRenewal={app.budgetRenewal}
|
||||
/>
|
||||
{app.expiresAt && (
|
||||
<p>Expires {new Date(app.expiresAt).toLocaleDateString()}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full">
|
||||
|
|
@ -390,17 +523,6 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) {
|
|||
}
|
||||
description={""}
|
||||
/>
|
||||
{!isEditingPermissions && (
|
||||
<>
|
||||
{appStoreApp && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<AboutAppCard appStoreApp={appStoreApp} />
|
||||
<AppLinksCard appStoreApp={appStoreApp} />
|
||||
</div>
|
||||
)}
|
||||
<AppUsage app={app} />
|
||||
</>
|
||||
)}
|
||||
{isEditingPermissions ? (
|
||||
<form
|
||||
id="app-details"
|
||||
|
|
@ -453,23 +575,43 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) {
|
|||
</Card>
|
||||
</form>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
Permissions
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Permissions
|
||||
capabilities={capabilities}
|
||||
permissions={savedPermissions}
|
||||
setPermissions={setPermissions}
|
||||
readOnly
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div
|
||||
className={cn(
|
||||
"grid grid-cols-1 gap-3 items-start",
|
||||
appStoreApp && "lg:grid-cols-3"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-3 min-w-0",
|
||||
appStoreApp && "lg:col-span-2"
|
||||
)}
|
||||
>
|
||||
<AppUsage app={app} />
|
||||
{!appStoreApp && (
|
||||
<Card>
|
||||
<CardContent className="py-6">
|
||||
{permissionsSection}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
<AppTransactionList appId={app.id} />
|
||||
</div>
|
||||
{appStoreApp && (
|
||||
<Card className="gap-4">
|
||||
<CardHeader>
|
||||
<CardTitle>About the App</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<p className="text-muted-foreground">
|
||||
{appStoreApp.extendedDescription}
|
||||
</p>
|
||||
<AppLinks appStoreApp={appStoreApp} />
|
||||
<div className="border-t pt-4">{permissionsSection}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isEditingPermissions && (
|
||||
<>
|
||||
|
|
@ -485,7 +627,6 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) {
|
|||
onClose={() => setShowDisconnectAppDialog(false)}
|
||||
/>
|
||||
)}
|
||||
<AppTransactionList appId={app.id} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue