291 lines
11 KiB
TypeScript
291 lines
11 KiB
TypeScript
import { useState } from 'react';
|
|
import { useSeoMeta } from '@unhead/react';
|
|
import { Link } from 'react-router-dom';
|
|
import { PageHeader } from '@/components/PageHeader';
|
|
import { WalletModal } from '@/components/WalletModal';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { useWallet } from '@/hooks/useWallet';
|
|
import { useNWC } from '@/hooks/useNWCContext';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { useCashu } from '@/hooks/wallet/useCashu';
|
|
import { useTranslation } from '@/contexts/language';
|
|
import { LoginArea } from '@/components/auth/LoginArea';
|
|
import { CashuWalletCard } from '@/components/wallet/CashuWalletCard';
|
|
import { EscrowKeyCard } from '@/components/wallet/EscrowKeyCard';
|
|
import { EscrowSettingsCard } from '@/components/wallet/EscrowSettingsCard';
|
|
import { NwcWalletCard } from '@/components/wallet/NwcWalletCard';
|
|
import { cn } from '@/lib/utils';
|
|
import {
|
|
Zap,
|
|
Settings,
|
|
CheckCircle,
|
|
XCircle,
|
|
Globe,
|
|
WalletMinimal,
|
|
Nut,
|
|
ChevronRight,
|
|
ExternalLink,
|
|
ChevronDown,
|
|
ChevronUp,
|
|
Wallet,
|
|
Coins,
|
|
} from 'lucide-react';
|
|
|
|
// ─── Wallet status bar ────────────────────────────────────────────────────────
|
|
|
|
function WalletStatusBar() {
|
|
const { t } = useTranslation();
|
|
const { webln } = useWallet();
|
|
const { connections } = useNWC();
|
|
const { isInitialized: hasCashu } = useCashu();
|
|
const hasNWC = connections.length > 0 && connections.some((c) => c.isConnected);
|
|
const isReady = webln || hasNWC || hasCashu;
|
|
|
|
return (
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
{/* Status pills */}
|
|
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
|
|
<StatusPill icon={Nut} label="Cashu" active={hasCashu} color="text-orange-400" activeBg="bg-orange-500/20 border-orange-500/30" />
|
|
<StatusPill icon={Globe} label="WebLN" active={!!webln} color="text-blue-400" activeBg="bg-blue-500/20 border-blue-500/30" />
|
|
<StatusPill icon={WalletMinimal} label="NWC" active={hasNWC} color="text-purple-400" activeBg="bg-purple-500/20 border-purple-500/30" />
|
|
</div>
|
|
|
|
{/* Manage wallets */}
|
|
<WalletModal>
|
|
<button className="ml-auto flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/8 active:scale-95 transition-all flex-shrink-0">
|
|
<Settings className="h-3.5 w-3.5 text-orange-400" />
|
|
<span className="text-xs font-bold text-white">{t('walletUi.page.manage')}</span>
|
|
{isReady
|
|
? <Badge className="bg-green-500/20 text-green-400 border-green-500/30 text-[9px] px-1 py-0 leading-none">ON</Badge>
|
|
: <ChevronRight className="h-3 w-3 text-muted-foreground" />
|
|
}
|
|
</button>
|
|
</WalletModal>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StatusPill({
|
|
icon: Icon, label, active, color, activeBg,
|
|
}: {
|
|
icon: React.ComponentType<{ className?: string }>;
|
|
label: string;
|
|
active: boolean;
|
|
color: string;
|
|
activeBg: string;
|
|
}) {
|
|
return (
|
|
<div className={cn(
|
|
'flex items-center gap-1 px-2 py-1 rounded-full border text-[10px] font-bold transition-all',
|
|
active ? `${activeBg} ${color}` : 'bg-white/3 border-white/8 text-gray-400'
|
|
)}>
|
|
<Icon className="h-2.5 w-2.5" />
|
|
{label}
|
|
{active
|
|
? <CheckCircle className="h-2.5 w-2.5 text-green-500" />
|
|
: <XCircle className="h-2.5 w-2.5 text-gray-700" />
|
|
}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── Get a wallet (collapsible) ───────────────────────────────────────────────
|
|
|
|
function GetAWallet() {
|
|
const { t } = useTranslation();
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const wallets = [
|
|
{ name: 'Minibits', desc: t('walletUi.page.walletMinibits'), url: 'https://www.minibits.cash', color: 'text-orange-400' },
|
|
{ name: 'Alby', desc: t('walletUi.page.walletAlby'), url: 'https://getalby.com', color: 'text-yellow-400' },
|
|
{ name: 'Zeus', desc: t('walletUi.page.walletZeus'), url: 'https://zeusln.com', color: 'text-blue-400' },
|
|
{ name: 'Coinos', desc: t('walletUi.page.walletCoinos'), url: 'https://coinos.io', color: 'text-green-400' },
|
|
];
|
|
|
|
return (
|
|
<div className="rounded-2xl border border-white/8 overflow-hidden">
|
|
<button
|
|
onClick={() => setOpen((v) => !v)}
|
|
className="w-full flex items-center justify-between px-4 py-3.5 bg-white/3 hover:bg-white/5 transition-all"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Zap className="h-4 w-4 text-yellow-400" />
|
|
<span className="text-sm font-black uppercase tracking-wide text-white">{t('walletUi.page.getAWallet')}</span>
|
|
</div>
|
|
{open
|
|
? <ChevronUp className="h-4 w-4 text-muted-foreground" />
|
|
: <ChevronDown className="h-4 w-4 text-muted-foreground" />
|
|
}
|
|
</button>
|
|
|
|
{open && (
|
|
<div className="divide-y divide-white/5">
|
|
{wallets.map((w) => (
|
|
<a
|
|
key={w.name}
|
|
href={w.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center justify-between px-4 py-3 hover:bg-white/5 active:scale-[0.99] transition-all"
|
|
>
|
|
<div className="min-w-0">
|
|
<p className={cn('font-bold text-sm', w.color)}>{w.name}</p>
|
|
<p className="text-xs text-muted-foreground">{w.desc}</p>
|
|
</div>
|
|
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
|
|
</a>
|
|
))}
|
|
<div className="px-4 py-3">
|
|
<a
|
|
href="https://planb.academy/en/tutorials/wallet"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-xs text-orange-400 hover:text-orange-300 transition-colors flex items-center gap-1.5"
|
|
>
|
|
<ExternalLink className="h-3 w-3" />
|
|
{t('walletUi.page.learnPlanB')}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── What are zaps (collapsible, hidden once wallet connected) ────────────────
|
|
|
|
function WhatAreZaps({ walletConnected }: { walletConnected: boolean }) {
|
|
const { t } = useTranslation();
|
|
const [open, setOpen] = useState(!walletConnected);
|
|
|
|
if (walletConnected && !open) {
|
|
return (
|
|
<button
|
|
onClick={() => setOpen(true)}
|
|
className="text-xs text-muted-foreground/40 hover:text-muted-foreground transition-colors flex items-center gap-1 mx-auto"
|
|
>
|
|
<ChevronDown className="h-3 w-3" />
|
|
{t('walletUi.page.whatAreZapsShort')}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="rounded-2xl border border-white/8 overflow-hidden">
|
|
<button
|
|
onClick={() => setOpen((v) => !v)}
|
|
className="w-full flex items-center justify-between px-4 py-3.5 bg-white/3 hover:bg-white/5 transition-all"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Zap className="h-4 w-4 text-amber-500" />
|
|
<span className="text-sm font-black uppercase tracking-wide text-white">{t('walletUi.page.whatAreZaps')}</span>
|
|
</div>
|
|
{open
|
|
? <ChevronUp className="h-4 w-4 text-muted-foreground" />
|
|
: <ChevronDown className="h-4 w-4 text-muted-foreground" />
|
|
}
|
|
</button>
|
|
|
|
{open && (
|
|
<div className="px-4 pb-4 pt-3 space-y-3">
|
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
|
{t('walletUi.page.zapsDesc')}
|
|
</p>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{[
|
|
{ emoji: '⚡', title: t('walletUi.page.instant'), desc: t('walletUi.page.instantDesc') },
|
|
{ emoji: '🔐', title: t('walletUi.page.flexible'), desc: t('walletUi.page.flexibleDesc') },
|
|
{ emoji: '🥜', title: t('walletUi.page.private'), desc: t('walletUi.page.privateDesc') },
|
|
{ emoji: '💜', title: t('walletUi.page.direct'), desc: t('walletUi.page.directDesc') },
|
|
].map((item) => (
|
|
<div key={item.title} className="flex items-start gap-2 p-2.5 bg-white/3 rounded-xl">
|
|
<span className="text-base flex-shrink-0">{item.emoji}</span>
|
|
<div>
|
|
<p className="font-bold text-xs text-white">{item.title}</p>
|
|
<p className="text-[11px] text-muted-foreground">{item.desc}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── Not logged in ────────────────────────────────────────────────────────────
|
|
|
|
function NotLoggedIn() {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<div className="flex flex-col items-center justify-center py-20 px-4 text-center">
|
|
<div className="w-16 h-16 rounded-2xl bg-orange-500/10 flex items-center justify-center mb-4">
|
|
<Wallet className="h-8 w-8 text-orange-500/60" />
|
|
</div>
|
|
<h3 className="text-lg font-black mb-2 text-white">{t('walletUi.page.loginRequired')}</h3>
|
|
<p className="text-muted-foreground text-sm max-w-xs mb-6 leading-relaxed">
|
|
{t('walletUi.page.loginRequiredDesc')}
|
|
</p>
|
|
<LoginArea className="justify-center" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── Page ─────────────────────────────────────────────────────────────────────
|
|
|
|
export default function WalletPage() {
|
|
const { t } = useTranslation();
|
|
useSeoMeta({
|
|
title: t('seoUi.walletTitle'),
|
|
description: t('seoUi.walletDesc'),
|
|
});
|
|
|
|
const { user } = useCurrentUser();
|
|
const { webln } = useWallet();
|
|
const { connections } = useNWC();
|
|
const { isInitialized: hasCashu } = useCashu();
|
|
const hasNWC = connections.length > 0 && connections.some((c) => c.isConnected);
|
|
const walletConnected = !!(webln || hasNWC || hasCashu);
|
|
|
|
return (
|
|
<>
|
|
<div className="container px-4 py-5 max-w-lg mx-auto">
|
|
|
|
<PageHeader title={t('walletUi.page.title')} accent="#ff6600" subtitle={t('walletUi.page.subtitle')} />
|
|
|
|
{user ? (
|
|
<div className="space-y-4">
|
|
{/* Status bar + manage — compact single row */}
|
|
<WalletStatusBar />
|
|
|
|
{/* Connected Lightning wallet (NIP-47) — balance / receive / send */}
|
|
<NwcWalletCard />
|
|
|
|
{/* Cashu wallet — native app, self-custodial ecash */}
|
|
<CashuWalletCard />
|
|
<EscrowKeyCard />
|
|
<EscrowSettingsCard />
|
|
|
|
<Link
|
|
to="/mints"
|
|
className="flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 transition-colors hover:bg-white/[0.06]"
|
|
>
|
|
<span className="flex items-center gap-2 text-sm font-medium text-white">
|
|
<Coins className="h-4 w-4 text-[#ccff00]" /> {t('walletUi.page.browseMints')}
|
|
</span>
|
|
<ChevronRight className="h-4 w-4 text-gray-500" />
|
|
</Link>
|
|
|
|
{/* Get a wallet — collapsed by default */}
|
|
<GetAWallet />
|
|
|
|
{/* What are zaps — auto-collapses once wallet connected */}
|
|
<WhatAreZaps walletConnected={walletConnected} />
|
|
</div>
|
|
) : (
|
|
<NotLoggedIn />
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|