Amber login (critical): - Replace window.location.href with window.open(_system) in Capacitor WebViews - Capacitor's WebView throws "malformed nostrsigner request" on href navigation - New openAmberUrl() helper detects Capacitor via window.Capacitor.isNativePlatform() - Add openAmberForLoginNoCallback() for PWA/Capacitor clipboard flow - Fix all 6 Amber URL opens in amber.ts + LoginDialog.tsx Mobile nav: - Newsletter and Sasquatch AI (/pyramid) added to hamburger menu - Sasquatch floating guide now shows on mobile (removed hidden md:block) Lab page mobile: - Action buttons (Offer Coaching/Filming/Editing/Get Card) now 2-col grid on mobile - Service type filter (Coaches/Filmers/Editors) now 3-col grid, full width - Reduced top padding on mobile (py-6 → py-12 on md+) - Text truncates cleanly, icons don't overflow Wallet page mobile: - Reduced padding (py-6 md:py-8) - Tab triggers use h-auto + py-2.5 to avoid overflow - Smaller header text on mobile Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
355 lines
12 KiB
TypeScript
355 lines
12 KiB
TypeScript
import { useSeoMeta } from '@unhead/react';
|
|
import { Layout } from '@/components/Layout';
|
|
import { WalletModal } from '@/components/WalletModal';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import { useWallet } from '@/hooks/useWallet';
|
|
import { useNWC } from '@/hooks/useNWCContext';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { useCashu } from '@/hooks/wallet/useCashu';
|
|
import { useSatsToUsd } from '@/hooks/useExchangeRate';
|
|
import { LoginArea } from '@/components/auth/LoginArea';
|
|
import { CashuWalletCard } from '@/components/wallet/CashuWalletCard';
|
|
import { LightningPrivacyTips } from '@/components/wallet/LightningPrivacyTips';
|
|
import { WalletComparisonGuide } from '@/components/wallet/WalletComparisonGuide';
|
|
import {
|
|
Wallet,
|
|
Zap,
|
|
Settings,
|
|
CheckCircle,
|
|
XCircle,
|
|
ExternalLink,
|
|
Globe,
|
|
WalletMinimal,
|
|
Info,
|
|
Nut,
|
|
} from 'lucide-react';
|
|
|
|
function WalletStatusCard() {
|
|
const { webln } = useWallet();
|
|
const { connections } = useNWC();
|
|
const { totalBalance: cashuBalance, isInitialized: hasCashu } = useCashu();
|
|
const cashuUsd = useSatsToUsd(cashuBalance);
|
|
const hasNWC = connections.length > 0 && connections.some(c => c.isConnected);
|
|
const isReady = webln || hasNWC || hasCashu;
|
|
|
|
return (
|
|
<Card className={isReady ? 'border-primary/30' : ''}>
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Wallet className="h-5 w-5 text-primary" />
|
|
Wallet Status
|
|
</CardTitle>
|
|
<Badge variant={isReady ? 'default' : 'secondary'}>
|
|
{isReady ? 'Ready' : 'Not Connected'}
|
|
</Badge>
|
|
</div>
|
|
<CardDescription>
|
|
{isReady
|
|
? 'Your wallet is connected and ready to send zaps!'
|
|
: 'Connect a wallet to enable Lightning zaps'}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* Cashu Wallet Status */}
|
|
<div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
<Nut className="h-5 w-5 text-orange-500" />
|
|
<div>
|
|
<p className="font-medium">Cashu Wallet</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{hasCashu && cashuBalance > 0
|
|
? `${cashuBalance.toLocaleString()} sats${cashuUsd ? ` (~$${cashuUsd.toFixed(2)})` : ''}`
|
|
: 'Built-in ecash wallet'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{hasCashu ? (
|
|
<CheckCircle className="h-5 w-5 text-green-500" />
|
|
) : (
|
|
<XCircle className="h-5 w-5 text-muted-foreground" />
|
|
)}
|
|
</div>
|
|
|
|
{/* WebLN Status */}
|
|
<div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
<Globe className="h-5 w-5 text-muted-foreground" />
|
|
<div>
|
|
<p className="font-medium">WebLN</p>
|
|
<p className="text-sm text-muted-foreground">Browser extension wallet</p>
|
|
</div>
|
|
</div>
|
|
{webln ? (
|
|
<CheckCircle className="h-5 w-5 text-green-500" />
|
|
) : (
|
|
<XCircle className="h-5 w-5 text-muted-foreground" />
|
|
)}
|
|
</div>
|
|
|
|
{/* NWC Status */}
|
|
<div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
<WalletMinimal className="h-5 w-5 text-muted-foreground" />
|
|
<div>
|
|
<p className="font-medium">Nostr Wallet Connect</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{connections.length > 0
|
|
? `${connections.length} wallet${connections.length !== 1 ? 's' : ''} connected`
|
|
: 'Remote wallet connection'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{hasNWC ? (
|
|
<CheckCircle className="h-5 w-5 text-green-500" />
|
|
) : (
|
|
<XCircle className="h-5 w-5 text-muted-foreground" />
|
|
)}
|
|
</div>
|
|
|
|
<WalletModal>
|
|
<Button className="w-full gap-2">
|
|
<Settings className="h-4 w-4" />
|
|
Manage Wallets
|
|
</Button>
|
|
</WalletModal>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function ZapsInfoCard() {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Zap className="h-5 w-5 text-amber-500" />
|
|
What are Zaps?
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<p className="text-muted-foreground">
|
|
Zaps are Lightning Network micropayments on Nostr. They allow you to send
|
|
satoshis (sats) instantly to other users as a way of tipping, thanking, or
|
|
supporting their content.
|
|
</p>
|
|
|
|
<div className="space-y-3">
|
|
<div className="flex items-start gap-3 p-3 bg-muted/50 rounded-lg">
|
|
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
|
|
<span className="text-lg">⚡</span>
|
|
</div>
|
|
<div>
|
|
<p className="font-medium">Instant</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
Payments settle in seconds over the Lightning Network
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-start gap-3 p-3 bg-muted/50 rounded-lg">
|
|
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
|
|
<span className="text-lg">🔐</span>
|
|
</div>
|
|
<div>
|
|
<p className="font-medium">Flexible custody</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
Use your own wallet (self-custodial) or a custodial service - your choice
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-start gap-3 p-3 bg-muted/50 rounded-lg">
|
|
<div className="w-8 h-8 rounded-full bg-orange-500/10 flex items-center justify-center flex-shrink-0">
|
|
<span className="text-lg">🥜</span>
|
|
</div>
|
|
<div>
|
|
<p className="font-medium">NutZaps for privacy</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
Use Cashu ecash for private zaps - mints can't see who pays whom
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-start gap-3 p-3 bg-muted/50 rounded-lg">
|
|
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
|
|
<span className="text-lg">💜</span>
|
|
</div>
|
|
<div>
|
|
<p className="font-medium">Value for value</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
Support creators directly for content you appreciate
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function WalletResourcesCard() {
|
|
const walletResources = [
|
|
{
|
|
name: 'Alby',
|
|
description: 'Browser extension wallet with WebLN + NWC support',
|
|
url: 'https://getalby.com',
|
|
type: 'WebLN',
|
|
},
|
|
{
|
|
name: 'Minibits',
|
|
description: 'Mobile Cashu wallet with NWC support',
|
|
url: 'https://www.minibits.cash',
|
|
type: 'NWC',
|
|
},
|
|
{
|
|
name: 'Zeus',
|
|
description: 'Self-custodial mobile Lightning wallet',
|
|
url: 'https://zeusln.com',
|
|
type: 'NWC',
|
|
},
|
|
{
|
|
name: 'Coinos',
|
|
description: 'Web-based Lightning wallet, works on any device',
|
|
url: 'https://coinos.io',
|
|
type: 'NWC',
|
|
},
|
|
];
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Info className="h-5 w-5" />
|
|
Recommended Wallets
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Get started with one of these Lightning wallets
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid gap-3">
|
|
{walletResources.map((wallet) => (
|
|
<a
|
|
key={wallet.name}
|
|
href={wallet.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center justify-between p-3 bg-muted/50 rounded-lg hover:bg-muted transition-colors group"
|
|
>
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<p className="font-medium group-hover:text-primary transition-colors">
|
|
{wallet.name}
|
|
</p>
|
|
<Badge variant="outline" className="text-xs">
|
|
{wallet.type}
|
|
</Badge>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">{wallet.description}</p>
|
|
</div>
|
|
<ExternalLink className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" />
|
|
</a>
|
|
))}
|
|
</div>
|
|
|
|
<a
|
|
href="https://planb.academy/en/tutorials/wallet"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center gap-2 text-sm text-orange-400 hover:text-orange-300 transition-colors"
|
|
>
|
|
<ExternalLink className="h-4 w-4" />
|
|
<span>Want to learn more? Check out Plan B Academy's wallet guides</span>
|
|
</a>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function NotLoggedInCard() {
|
|
return (
|
|
<Card className="border-dashed">
|
|
<CardContent className="py-12 text-center">
|
|
<Wallet className="h-12 w-12 mx-auto mb-4 text-muted-foreground/50" />
|
|
<h3 className="text-lg font-medium mb-2">Login Required</h3>
|
|
<p className="text-muted-foreground max-w-md mx-auto mb-6">
|
|
Log in with your Nostr account to connect your Lightning wallet and start zapping.
|
|
</p>
|
|
<LoginArea className="justify-center" />
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export default function WalletPage() {
|
|
useSeoMeta({
|
|
title: 'Wallet - SkateSpots',
|
|
description: 'Connect your Lightning wallet to send zaps to skaters and spot creators.',
|
|
});
|
|
|
|
const { user } = useCurrentUser();
|
|
|
|
return (
|
|
<Layout>
|
|
<div className="container px-4 py-6 md:py-8">
|
|
{/* Header */}
|
|
<div className="mb-6 md:mb-8">
|
|
<h1 className="text-2xl md:text-3xl font-bold">Wallet</h1>
|
|
<p className="text-muted-foreground mt-1 text-sm md:text-base">
|
|
Manage your Bitcoin and send zaps to skaters and creators
|
|
</p>
|
|
</div>
|
|
|
|
{user ? (
|
|
<div className="space-y-4 md:space-y-6">
|
|
{/* Wallet Comparison Guide */}
|
|
<WalletComparisonGuide />
|
|
|
|
<Tabs defaultValue="cashu" className="w-full">
|
|
<TabsList className="grid w-full grid-cols-2 mb-4 md:mb-6 h-auto">
|
|
<TabsTrigger value="cashu" className="gap-1.5 py-2.5 text-sm">
|
|
<Nut className="h-4 w-4 flex-shrink-0" />
|
|
<span>Cashu Wallet</span>
|
|
</TabsTrigger>
|
|
<TabsTrigger value="lightning" className="gap-1.5 py-2.5 text-sm">
|
|
<Zap className="h-4 w-4 flex-shrink-0" />
|
|
<span>Lightning</span>
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="cashu" className="space-y-6">
|
|
<CashuWalletCard />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="lightning" className="space-y-6">
|
|
{/* Privacy Tips at the top */}
|
|
<LightningPrivacyTips />
|
|
|
|
<div className="grid gap-6 lg:grid-cols-2">
|
|
<div className="space-y-6">
|
|
<WalletStatusCard />
|
|
<WalletResourcesCard />
|
|
</div>
|
|
<div>
|
|
<ZapsInfoCard />
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
) : (
|
|
<div className="max-w-xl mx-auto">
|
|
<NotLoggedInCard />
|
|
<div className="mt-6">
|
|
<ZapsInfoCard />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Layout>
|
|
);
|
|
}
|