SkateSpots/src/pages/WalletPage.tsx
shakespeare.diy a677e8687d fix: Amber Capacitor APK + mobile UI improvements
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>
2026-06-18 20:48:11 +07:00

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>
);
}