mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
feat(frontend): move ZapPlanner button to Wallet and move wallet mobile actions to overflow menu (#2200)
* feat(frontend): improve wallet page balance layout Center the wallet balance area and tune spacing for a cleaner visual rhythm. Move secondary actions to the header as ghost buttons and use a vertical more icon. Made-with: Cursor * feat(frontend): refine wallet header actions on mobile Move wallet secondary actions into the mobile overflow menu and add Recurring. Also remove the ZapPlanner card from Home to avoid duplicate entry points. Made-with: Cursor * refactor(frontend): separate wallet actions, add ProDropdownMenuItem and AlertAction - Extract wallet navigation (Swap, Recurring, Buy) into dedicated WalletActionsMenu component - Revert TransactionsListMenu to single-purpose (export transactions only) - Move CSV export logic to shared transactions-utils - Add ProDropdownMenuItem for reusable pro-gated dropdown items with consistent Pro badge - Add AlertAction component to alert system for proper action button placement - Use controlled mode in UpgradeDialog to avoid DialogTrigger data-slot conflicts - Replace inline upgrade gating in Channels (Set Node Alias) and Settings (themes) - Theme select now opens UpgradeDialog instead of disabling paid items - Use AlertAction in SubwalletList for upgrade prompt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(frontend): consolidate export into WalletActionsMenu Show wallet actions (Swap, Recurring, Buy) only on mobile, Export Transactions on all breakpoints — removes the separate TransactionsListMenu from the wallet page. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(frontend): sanitize CSV export and fix subwallet upgrade copy Prevent CSV formula injection by prepending a single quote to values starting with =, +, -, or @. Fix grammar in sub-wallet upgrade prompt. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(frontend): center fiat amount skeleton on wallet page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: René Aaron <rene@twentyuno.net> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ac88c8d573
commit
46d3a8942a
10 changed files with 298 additions and 214 deletions
|
|
@ -1,96 +1,14 @@
|
|||
import { DownloadIcon, EllipsisVerticalIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "src/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "src/components/ui/dropdown-menu";
|
||||
import { UpgradeDialog } from "src/components/UpgradeDialog";
|
||||
import { LIST_TRANSACTIONS_LIMIT } from "src/constants";
|
||||
import { useAlbyMe } from "src/hooks/useAlbyMe";
|
||||
import { ListTransactionsResponse, Transaction } from "src/types";
|
||||
import { request } from "src/utils/request";
|
||||
|
||||
const convertToCSV = (transactions: Transaction[]) => {
|
||||
if (!transactions.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Get headers from all transactions
|
||||
const headers = Object.keys(transactions[0]);
|
||||
const csvHeaders = headers.join(",");
|
||||
|
||||
// Convert each transaction to CSV row
|
||||
const csvRows = transactions.map((tx) => {
|
||||
return headers
|
||||
.map((header) => {
|
||||
const value = tx[header as keyof typeof tx];
|
||||
if (value === undefined || value === null) {
|
||||
return "";
|
||||
}
|
||||
const stringValue =
|
||||
typeof value === "object" ? JSON.stringify(value) : String(value);
|
||||
// based on https://stackoverflow.com/a/68146412
|
||||
return `"${stringValue.replaceAll('"', '""')}"`; // escape double quotes
|
||||
})
|
||||
.join(",");
|
||||
});
|
||||
|
||||
return [csvHeaders, ...csvRows].join("\n");
|
||||
};
|
||||
|
||||
const handleExportTransactions = async (appId?: number) => {
|
||||
try {
|
||||
// Fetch all transactions by paginating through all pages
|
||||
let allTransactions: Transaction[] = [];
|
||||
let offset = 0;
|
||||
|
||||
while (true) {
|
||||
let url = `/api/transactions?limit=${LIST_TRANSACTIONS_LIMIT}&offset=${offset}`;
|
||||
if (appId) {
|
||||
url += `&appId=${appId}`;
|
||||
}
|
||||
|
||||
const data = await request<ListTransactionsResponse>(url);
|
||||
|
||||
if (!data) {
|
||||
throw new Error("no list transactions response");
|
||||
}
|
||||
|
||||
allTransactions = [...allTransactions, ...data.transactions];
|
||||
|
||||
if (data.transactions.length < LIST_TRANSACTIONS_LIMIT) {
|
||||
break;
|
||||
}
|
||||
offset += LIST_TRANSACTIONS_LIMIT;
|
||||
}
|
||||
|
||||
// Convert to CSV and create download
|
||||
const csvString = convertToCSV(allTransactions);
|
||||
const blob = new Blob([csvString], { type: "text/csv" });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
const filename = appId
|
||||
? `transactions_app_${appId}.csv`
|
||||
: `transactions_all.csv`;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
toast("Transactions saved to your downloads folder");
|
||||
} catch (error) {
|
||||
console.error("Error downloading transactions:", error);
|
||||
toast.error("Failed to export transactions");
|
||||
}
|
||||
};
|
||||
import { ProDropdownMenuItem } from "src/components/UpgradeDialog";
|
||||
import { handleExportTransactions } from "./transactions-utils";
|
||||
|
||||
export const TransactionsListMenu = ({ appId }: { appId?: number }) => {
|
||||
const { data: albyMe } = useAlbyMe();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<Button asChild size="icon" variant="ghost">
|
||||
|
|
@ -99,26 +17,10 @@ export const TransactionsListMenu = ({ appId }: { appId?: number }) => {
|
|||
</DropdownMenuTrigger>
|
||||
</Button>
|
||||
<DropdownMenuContent align="end">
|
||||
{!albyMe?.subscription.plan_code ? (
|
||||
<UpgradeDialog>
|
||||
<div className="cursor-pointer">
|
||||
<DropdownMenuItem className="w-full pointer-events-none">
|
||||
<div className="w-full flex items-center">
|
||||
<DownloadIcon className="h-4 w-4 mr-2" />
|
||||
Export Transactions
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</UpgradeDialog>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
className="flex flex-row items-center gap-2 cursor-pointer"
|
||||
onClick={() => handleExportTransactions(appId)}
|
||||
>
|
||||
<DownloadIcon className="h-4 w-4" />
|
||||
Export Transactions
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<ProDropdownMenuItem onClick={() => handleExportTransactions(appId)}>
|
||||
<DownloadIcon className="h-4 w-4" />
|
||||
Export Transactions
|
||||
</ProDropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,27 +3,77 @@ import {
|
|||
MailIcon,
|
||||
RefreshCwIcon,
|
||||
SparklesIcon,
|
||||
StarsIcon,
|
||||
UsersIcon,
|
||||
ZapIcon,
|
||||
} from "lucide-react";
|
||||
import { ReactNode } from "react";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { Badge } from "src/components/ui/badge";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "src/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "src/components/ui/dropdown-menu";
|
||||
import { useAlbyMe } from "src/hooks/useAlbyMe";
|
||||
import { useInfo } from "src/hooks/useInfo";
|
||||
import { ExternalLinkButton } from "./ui/custom/external-link-button";
|
||||
|
||||
interface UpgradeDialogProps {
|
||||
children: ReactNode;
|
||||
children?: ReactNode;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const UpgradeDialog = ({ children }: UpgradeDialogProps) => {
|
||||
interface ProDropdownMenuItemProps {
|
||||
children: ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export function ProDropdownMenuItem({
|
||||
children,
|
||||
onClick,
|
||||
}: ProDropdownMenuItemProps) {
|
||||
const { data: albyMe } = useAlbyMe();
|
||||
|
||||
if (!albyMe?.subscription.plan_code) {
|
||||
return (
|
||||
<UpgradeDialog>
|
||||
<div className="cursor-pointer">
|
||||
<DropdownMenuItem className="w-full pointer-events-none">
|
||||
{children}
|
||||
<Badge variant="outline">
|
||||
<StarsIcon />
|
||||
Pro
|
||||
</Badge>
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</UpgradeDialog>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuItem className="cursor-pointer" onClick={onClick}>
|
||||
{children}
|
||||
<Badge variant="outline">
|
||||
<StarsIcon />
|
||||
Pro
|
||||
</Badge>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export const UpgradeDialog = ({
|
||||
children,
|
||||
open: controlledOpen,
|
||||
onOpenChange: controlledOnOpenChange,
|
||||
}: UpgradeDialogProps) => {
|
||||
const [internalOpen, setInternalOpen] = useState(false);
|
||||
const open = controlledOpen ?? internalOpen;
|
||||
const setOpen = controlledOnOpenChange ?? setInternalOpen;
|
||||
|
||||
const { data: albyMe } = useAlbyMe();
|
||||
const { data: info } = useInfo();
|
||||
|
||||
|
|
@ -37,8 +87,12 @@ export const UpgradeDialog = ({ children }: UpgradeDialogProps) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
{children && (
|
||||
<span onClick={() => setOpen(true)} className="cursor-pointer">
|
||||
{children}
|
||||
</span>
|
||||
)}
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex flex-row gap-2 items-center">
|
||||
|
|
|
|||
70
frontend/src/components/WalletActionsMenu.tsx
Normal file
70
frontend/src/components/WalletActionsMenu.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import {
|
||||
ArrowDownUpIcon,
|
||||
CalendarSyncIcon,
|
||||
CreditCardIcon,
|
||||
DownloadIcon,
|
||||
EllipsisVerticalIcon,
|
||||
} from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import ExternalLink from "src/components/ExternalLink";
|
||||
import { Button } from "src/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "src/components/ui/dropdown-menu";
|
||||
import { ProDropdownMenuItem } from "src/components/UpgradeDialog";
|
||||
import { handleExportTransactions } from "./transactions-utils";
|
||||
|
||||
export function WalletActionsMenu({
|
||||
hasChannelManagement,
|
||||
}: {
|
||||
hasChannelManagement: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<Button asChild size="icon" variant="ghost">
|
||||
<DropdownMenuTrigger>
|
||||
<EllipsisVerticalIcon className="h-4 w-4" />
|
||||
</DropdownMenuTrigger>
|
||||
</Button>
|
||||
<DropdownMenuContent align="end">
|
||||
<div className="sm:hidden">
|
||||
{hasChannelManagement && (
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/wallet/swap" className="w-full cursor-pointer">
|
||||
<ArrowDownUpIcon className="h-4 w-4" />
|
||||
Swap
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
to="/internal-apps/zapplanner"
|
||||
className="w-full cursor-pointer"
|
||||
>
|
||||
<CalendarSyncIcon className="h-4 w-4" />
|
||||
Recurring
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<ExternalLink
|
||||
to="https://www.getalby.com/topup"
|
||||
className="w-full cursor-pointer"
|
||||
>
|
||||
<CreditCardIcon className="h-4 w-4" />
|
||||
Buy
|
||||
</ExternalLink>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</div>
|
||||
<ProDropdownMenuItem onClick={() => handleExportTransactions()}>
|
||||
<DownloadIcon className="h-4 w-4" />
|
||||
Export Transactions
|
||||
</ProDropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
82
frontend/src/components/transactions-utils.tsx
Normal file
82
frontend/src/components/transactions-utils.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { toast } from "sonner";
|
||||
import { LIST_TRANSACTIONS_LIMIT } from "src/constants";
|
||||
import { ListTransactionsResponse, Transaction } from "src/types";
|
||||
import { request } from "src/utils/request";
|
||||
|
||||
export const convertToCSV = (transactions: Transaction[]) => {
|
||||
if (!transactions.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Get headers from all transactions
|
||||
const headers = Object.keys(transactions[0]);
|
||||
const csvHeaders = headers.join(",");
|
||||
|
||||
// Convert each transaction to CSV row
|
||||
const csvRows = transactions.map((tx) => {
|
||||
return headers
|
||||
.map((header) => {
|
||||
const value = tx[header as keyof typeof tx];
|
||||
if (value === undefined || value === null) {
|
||||
return "";
|
||||
}
|
||||
const stringValue =
|
||||
typeof value === "object" ? JSON.stringify(value) : String(value);
|
||||
const safeValue = /^[\t\r ]*[=+\-@]/.test(stringValue)
|
||||
? `'${stringValue}`
|
||||
: stringValue;
|
||||
// based on https://stackoverflow.com/a/68146412
|
||||
return `"${safeValue.replaceAll('"', '""')}"`; // escape double quotes
|
||||
})
|
||||
.join(",");
|
||||
});
|
||||
|
||||
return [csvHeaders, ...csvRows].join("\n");
|
||||
};
|
||||
|
||||
export const handleExportTransactions = async (appId?: number) => {
|
||||
try {
|
||||
// Fetch all transactions by paginating through all pages
|
||||
let allTransactions: Transaction[] = [];
|
||||
let offset = 0;
|
||||
|
||||
while (true) {
|
||||
let url = `/api/transactions?limit=${LIST_TRANSACTIONS_LIMIT}&offset=${offset}`;
|
||||
if (appId) {
|
||||
url += `&appId=${appId}`;
|
||||
}
|
||||
|
||||
const data = await request<ListTransactionsResponse>(url);
|
||||
|
||||
if (!data) {
|
||||
throw new Error("no list transactions response");
|
||||
}
|
||||
|
||||
allTransactions = [...allTransactions, ...data.transactions];
|
||||
|
||||
if (data.transactions.length < LIST_TRANSACTIONS_LIMIT) {
|
||||
break;
|
||||
}
|
||||
offset += LIST_TRANSACTIONS_LIMIT;
|
||||
}
|
||||
|
||||
// Convert to CSV and create download
|
||||
const csvString = convertToCSV(allTransactions);
|
||||
const blob = new Blob([csvString], { type: "text/csv" });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
const filename = appId
|
||||
? `transactions_app_${appId}.csv`
|
||||
: `transactions_all.csv`;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
toast("Transactions saved to your downloads folder");
|
||||
} catch (error) {
|
||||
console.error("Error downloading transactions:", error);
|
||||
toast.error("Failed to export transactions");
|
||||
}
|
||||
};
|
||||
|
|
@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|||
import { cn } from "src/lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
"relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current has-[[data-slot=alert-action]]:grid-cols-[0_1fr_auto] has-[>svg]:has-[[data-slot=alert-action]]:grid-cols-[calc(var(--spacing)*4)_1fr_auto]",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
@ -65,4 +65,14 @@ function AlertDescription({
|
|||
);
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-action"
|
||||
className={cn("col-start-3 row-start-1 row-span-2 self-start", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription, AlertAction };
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import AppHeader from "src/components/AppHeader";
|
|||
import ExternalLink from "src/components/ExternalLink";
|
||||
import { AlbyHead } from "src/components/images/AlbyHead";
|
||||
import Loading from "src/components/Loading";
|
||||
import { Badge } from "src/components/ui/badge";
|
||||
import { Button } from "src/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
|
|
@ -20,7 +19,6 @@ import OnboardingChecklist from "src/screens/wallet/OnboardingChecklist";
|
|||
|
||||
import React from "react";
|
||||
import albyGo from "src/assets/suggested-apps/alby-go.png";
|
||||
import zapplanner from "src/assets/suggested-apps/zapplanner.png";
|
||||
import { AppOfTheDayWidget } from "src/components/home/widgets/AppOfTheDayWidget";
|
||||
import { BlockHeightWidget } from "src/components/home/widgets/BlockHeightWidget";
|
||||
import { ForwardsWidget } from "src/components/home/widgets/ForwardsWidget";
|
||||
|
|
@ -168,34 +166,6 @@ function Home() {
|
|||
<div className="grid gap-3">
|
||||
<LightningMessageboardWidget />
|
||||
|
||||
<Link to="/internal-apps/zapplanner">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="shrink-0">
|
||||
<img
|
||||
src={zapplanner}
|
||||
className="w-12 h-12 rounded-xl border"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>
|
||||
<div className="flex-1 leading-5 font-semibold text-xl whitespace-nowrap text-ellipsis overflow-hidden ml-4 flex gap-2">
|
||||
ZapPlanner <Badge>NEW</Badge>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<CardDescription className="ml-4">
|
||||
Schedule automatic recurring lightning payments.
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="text-right">
|
||||
<Button variant="outline">Open</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-center">
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import {
|
|||
InfoIcon,
|
||||
LinkIcon,
|
||||
Settings2Icon,
|
||||
SparklesIcon,
|
||||
UnplugIcon,
|
||||
ZapIcon,
|
||||
} from "lucide-react";
|
||||
|
|
@ -57,9 +56,8 @@ import {
|
|||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "src/components/ui/tooltip.tsx";
|
||||
import { UpgradeDialog } from "src/components/UpgradeDialog";
|
||||
import { ProDropdownMenuItem } from "src/components/UpgradeDialog";
|
||||
import { ONCHAIN_DUST_SATS } from "src/constants.ts";
|
||||
import { useAlbyMe } from "src/hooks/useAlbyMe";
|
||||
import { useBalances } from "src/hooks/useBalances.ts";
|
||||
import { useChannels } from "src/hooks/useChannels";
|
||||
import { useInfo } from "src/hooks/useInfo";
|
||||
|
|
@ -78,7 +76,6 @@ import { request } from "src/utils/request";
|
|||
|
||||
export default function Channels() {
|
||||
useSyncWallet();
|
||||
const { data: albyMe } = useAlbyMe();
|
||||
const { data: channels } = useChannels();
|
||||
const { data: nodeConnectionInfo } = useNodeConnectionInfo();
|
||||
const { data: info, hasChannelManagement } = useInfo();
|
||||
|
|
@ -270,28 +267,13 @@ export default function Channels() {
|
|||
Sign Message
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{info?.backendType === "LDK" &&
|
||||
(!albyMe?.subscription.plan_code ? (
|
||||
<UpgradeDialog>
|
||||
<div className="cursor-pointer">
|
||||
<DropdownMenuItem className="w-full pointer-events-none">
|
||||
<Link
|
||||
className="w-full flex items-center"
|
||||
to="/wallet/node-alias"
|
||||
>
|
||||
<SparklesIcon className="size-4 mr-2" /> Set
|
||||
Node Alias
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</UpgradeDialog>
|
||||
) : (
|
||||
<DropdownMenuItem className="w-full">
|
||||
<Link className="w-full" to="/wallet/node-alias">
|
||||
Set Node Alias
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{info?.backendType === "LDK" && (
|
||||
<ProDropdownMenuItem
|
||||
onClick={() => navigate("/wallet/node-alias")}
|
||||
>
|
||||
Set Node Alias
|
||||
</ProDropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { StarsIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { toast } from "sonner";
|
||||
import Loading from "src/components/Loading";
|
||||
import SettingsHeader from "src/components/SettingsHeader";
|
||||
import { StarsIcon } from "lucide-react";
|
||||
import { UpgradeDialog } from "src/components/UpgradeDialog";
|
||||
import { Badge } from "src/components/ui/badge";
|
||||
import { Label } from "src/components/ui/label";
|
||||
import {
|
||||
|
|
@ -24,7 +26,6 @@ import {
|
|||
import { useAlbyMe } from "src/hooks/useAlbyMe";
|
||||
import { useCurrencies } from "src/hooks/useCurrencies";
|
||||
import { useInfo } from "src/hooks/useInfo";
|
||||
import { cn } from "src/lib/utils";
|
||||
import { handleRequestError } from "src/utils/handleRequestError";
|
||||
import { request } from "src/utils/request";
|
||||
|
||||
|
|
@ -32,6 +33,7 @@ function Settings() {
|
|||
const { data: albyMe } = useAlbyMe();
|
||||
const { theme, darkMode, setTheme, setDarkMode } = useTheme();
|
||||
const { currencies, isLoading: isCurrenciesLoading } = useCurrencies();
|
||||
const [showUpgradeDialog, setShowUpgradeDialog] = React.useState(false);
|
||||
|
||||
const { data: info, mutate: reloadInfo } = useInfo();
|
||||
|
||||
|
|
@ -96,6 +98,10 @@ function Settings() {
|
|||
<Select
|
||||
value={theme}
|
||||
onValueChange={(value) => {
|
||||
if (paidThemes.includes(value) && !hasPlan) {
|
||||
setShowUpgradeDialog(true);
|
||||
return;
|
||||
}
|
||||
setTheme(value as Theme);
|
||||
toast("Theme updated.");
|
||||
}}
|
||||
|
|
@ -106,23 +112,11 @@ function Settings() {
|
|||
<SelectContent>
|
||||
{Themes.map((theme) => {
|
||||
const isPaidTheme = paidThemes.includes(theme);
|
||||
const isDisabled = isPaidTheme && !hasPlan;
|
||||
|
||||
return (
|
||||
<SelectItem
|
||||
key={theme}
|
||||
value={theme}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<SelectItem key={theme} value={theme}>
|
||||
<div className="flex items-center justify-between gap-2 w-full">
|
||||
<span
|
||||
className={cn(
|
||||
"capitalize",
|
||||
isDisabled && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{theme}
|
||||
</span>
|
||||
<span className="capitalize">{theme}</span>
|
||||
{isPaidTheme && (
|
||||
<Badge variant="outline">
|
||||
<StarsIcon />
|
||||
|
|
@ -135,6 +129,10 @@ function Settings() {
|
|||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<UpgradeDialog
|
||||
open={showUpgradeDialog}
|
||||
onOpenChange={setShowUpgradeDialog}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="appearance">Appearance</Label>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,12 @@ import FormattedFiatAmount from "src/components/FormattedFiatAmount";
|
|||
import Loading from "src/components/Loading";
|
||||
import ResponsiveButton from "src/components/ResponsiveButton";
|
||||
import ResponsiveLinkButton from "src/components/ResponsiveLinkButton";
|
||||
import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert";
|
||||
import {
|
||||
Alert,
|
||||
AlertAction,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "src/components/ui/alert";
|
||||
import { Button } from "src/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
|
|
@ -108,24 +113,21 @@ export function SubwalletList() {
|
|||
|
||||
{!albyMe?.subscription.plan_code &&
|
||||
subwalletAppsData.totalCount >= MAX_FREE_SUBWALLETS && (
|
||||
<>
|
||||
<Alert>
|
||||
<InfoIcon />
|
||||
<AlertTitle>Need more Sub-wallets?</AlertTitle>
|
||||
<AlertDescription className="flex flex-row gap-3">
|
||||
<p className="grow">
|
||||
Upgrade your subscription plan to Pro unlock unlimited number
|
||||
of Sub-wallets.
|
||||
</p>
|
||||
<UpgradeDialog>
|
||||
<Button>
|
||||
<SparklesIcon />
|
||||
Upgrade
|
||||
</Button>
|
||||
</UpgradeDialog>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</>
|
||||
<Alert>
|
||||
<InfoIcon />
|
||||
<AlertTitle>Need more Sub-wallets?</AlertTitle>
|
||||
<AlertDescription>
|
||||
Upgrade to Pro for unlimited sub-wallets.
|
||||
</AlertDescription>
|
||||
<AlertAction>
|
||||
<UpgradeDialog>
|
||||
<Button size="sm">
|
||||
<SparklesIcon />
|
||||
Upgrade
|
||||
</Button>
|
||||
</UpgradeDialog>
|
||||
</AlertAction>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{!isSufficientlyBacked && (
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
ArrowDownIcon,
|
||||
ArrowDownUpIcon,
|
||||
ArrowUpIcon,
|
||||
CalendarSyncIcon,
|
||||
CreditCardIcon,
|
||||
ExternalLinkIcon,
|
||||
LightbulbIcon,
|
||||
|
|
@ -15,7 +16,7 @@ import FormattedFiatAmount from "src/components/FormattedFiatAmount";
|
|||
import Loading from "src/components/Loading";
|
||||
import LowReceivingCapacityAlert from "src/components/LowReceivingCapacityAlert";
|
||||
import TransactionsList from "src/components/TransactionsList";
|
||||
import { TransactionsListMenu } from "src/components/TransactionsListMenu";
|
||||
import { WalletActionsMenu } from "src/components/WalletActionsMenu";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
|
|
@ -45,23 +46,36 @@ function Wallet() {
|
|||
description=""
|
||||
contentRight={
|
||||
<div className="flex items-center gap-1 sm:gap-2">
|
||||
{hasChannelManagement && (
|
||||
<LinkButton
|
||||
to="/wallet/swap"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="hidden sm:inline-flex"
|
||||
>
|
||||
<ArrowDownUpIcon />
|
||||
Swap
|
||||
</LinkButton>
|
||||
)}
|
||||
<LinkButton
|
||||
to="/internal-apps/zapplanner"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="hidden sm:inline-flex"
|
||||
>
|
||||
<CalendarSyncIcon />
|
||||
Recurring
|
||||
</LinkButton>
|
||||
<ExternalLinkButton
|
||||
to="https://www.getalby.com/topup"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="hidden sm:inline-flex"
|
||||
>
|
||||
<CreditCardIcon />
|
||||
<span className="sr-only sm:not-sr-only sm:inline">
|
||||
Buy Bitcoin
|
||||
</span>
|
||||
Buy
|
||||
</ExternalLinkButton>
|
||||
{hasChannelManagement && (
|
||||
<LinkButton to="/wallet/swap" variant="ghost" size="sm">
|
||||
<ArrowDownUpIcon />
|
||||
<span className="sr-only sm:not-sr-only sm:inline">Swap</span>
|
||||
</LinkButton>
|
||||
)}
|
||||
<TransactionsListMenu />
|
||||
<WalletActionsMenu hasChannelManagement={!!hasChannelManagement} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue