mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-13 17:07:27 +02:00
* fix(nwc): improve connection reliability and add health tracking - Add connection status observable (disconnected/connecting/connected/error) - Validate wallet connection on restore using support$ observable with 10s timeout - Add notification subscription error recovery with exponential backoff (5 retries) - Add retry logic for balance refresh (3 retries with backoff) - Use library's support$ observable for wallet capabilities (cached by applesauce) - Replace manual getInfo() calls with reactive support$ subscription - Add visual connection status indicator in WalletViewer header - Add reconnect button when connection is in error state - Store network info in cached connection for display https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ * feat(wallet): add copy NWC connection string button to header Adds a copy button (Copy/Check icons) in the wallet header before the refresh button that copies the NWC connection string to clipboard for easy sharing or backup. https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ * fix(wallet): use CopyCheck icon and fix transaction loading - Change Check to CopyCheck icon for copy NWC string button - Add walletMethods computed value that combines support$ observable with cached info fallback from initial connection - Fix transaction history not loading because support$ waits for kind 13194 events which many NWC wallets don't publish - The cached info from getInfo() RPC call is now used as fallback https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ * refactor(nwc): simplify with derived state from observables Production-ready refactor of NWC implementation: nwc.ts: - Add wallet$ observable for reactive wallet instance access - Remove redundant subscribeToSupport() - only needed for validation - Cleaner code organization with clear sections useWallet.ts: - All state derived from observables (no useState for wallet) - Move walletMethods computation to hook (reusable) - isConnected derived from connectionStatus - Simplified from 240 to 170 lines WalletViewer.tsx: - Use walletMethods from hook instead of local useMemo - Simpler connection state tracking via connectionStatus - Remove redundant wallet variable from destructuring - No color change on copy NWC string (per feedback) https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ * refactor(wallet): use useCopy hook for clipboard operations Replace manual clipboard state management with useCopy hook: - copyInvoice/invoiceCopied for generated invoice - copyRawTx/rawTxCopied for transaction JSON - copyNwc/nwcCopied for NWC connection string Benefits: - Cleaner code (removed manual setTimeout calls) - Automatic timeout cleanup on unmount - Consistent copy behavior across all clipboard operations https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ * feat(wallet): implement lazy-loaded transactions observable - Add shared wallet types (Transaction, TransactionsState) in src/types/wallet.ts - Add transactionsState$ observable to NWC service for shared tx state - Implement loadTransactions, loadMoreTransactions, and retryLoadTransactions - Auto-refresh transactions on payment notifications - Simplify WalletViewer to use observable state instead of local state - Remove manual transaction loading logic from component https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ * fix(wallet): update balance observable on initial connect - Call refreshBalance() in createWalletFromURI to fetch initial balance - Update balance$ directly when ConnectWalletDialog gets balance - Fixes issue where WalletViewer showed "-" after connecting while user menu showed correct balance (different data sources) https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ * refactor(wallet): use single data source for balance across UI Remove fallback to nwcConnection.balance in user-menu - now both WalletViewer and user-menu use balance$ observable as the single source of truth for wallet balance. https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ * fix(wallet): address code review issues and simplify user menu - Fix memory leak: track retry timeout and clear on disconnect - Add explicit WalletSupport type for support observable - Add comments explaining balance refresh error handling behavior - Add comment about restoreWallet not being awaited intentionally - User menu now uses connectionStatus observable (shows connecting/error states) - Remove wallet name display from user menu (simplifies UI) - Remove unused walletServiceProfile hook and getWalletName function https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ * refactor(wallet): extract WalletConnectionStatus component - Create reusable WalletConnectionStatus component for connection indicator - Remove rounded borders from indicator (now square) - Export getConnectionStatusColor helper for custom usage - Use component in both user-menu and WalletViewer - Supports size (sm/md), showLabel, and className props https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ --------- Co-authored-by: Claude <noreply@anthropic.com>
200 lines
5.9 KiB
TypeScript
200 lines
5.9 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { toast } from "sonner";
|
|
import { Loader2, Wallet, AlertCircle, AlertTriangle } from "lucide-react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { useGrimoire } from "@/core/state";
|
|
import { createWalletFromURI, balance$ } from "@/services/nwc";
|
|
|
|
interface ConnectWalletDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onConnected?: () => void;
|
|
}
|
|
|
|
export default function ConnectWalletDialog({
|
|
open,
|
|
onOpenChange,
|
|
onConnected,
|
|
}: ConnectWalletDialogProps) {
|
|
const [connectionString, setConnectionString] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const { setNWCConnection, updateNWCBalance, updateNWCInfo } = useGrimoire();
|
|
|
|
// Reset state when dialog closes
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setConnectionString("");
|
|
setLoading(false);
|
|
setError(null);
|
|
}
|
|
}, [open]);
|
|
|
|
async function handleConnect() {
|
|
if (!connectionString.trim()) {
|
|
setError("Please enter a connection string");
|
|
return;
|
|
}
|
|
|
|
if (!connectionString.startsWith("nostr+walletconnect://")) {
|
|
setError(
|
|
"Invalid connection string. Must start with nostr+walletconnect://",
|
|
);
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
// Create wallet instance from connection string
|
|
const wallet = createWalletFromURI(connectionString);
|
|
|
|
// Test the connection by getting wallet info
|
|
const info = await wallet.getInfo();
|
|
|
|
// Get initial balance
|
|
let balance: number | undefined;
|
|
try {
|
|
const balanceResult = await wallet.getBalance();
|
|
balance = balanceResult.balance;
|
|
// Update the observable immediately so WalletViewer shows correct balance
|
|
balance$.next(balance);
|
|
} catch (err) {
|
|
console.warn("[NWC] Failed to get balance:", err);
|
|
// Balance is optional, continue anyway
|
|
}
|
|
|
|
// Get connection details from the wallet instance
|
|
const serialized = wallet.toJSON();
|
|
|
|
// Save connection to state
|
|
setNWCConnection({
|
|
service: serialized.service,
|
|
relays: serialized.relays,
|
|
secret: serialized.secret,
|
|
lud16: serialized.lud16,
|
|
balance,
|
|
info: {
|
|
alias: info.alias,
|
|
network: info.network,
|
|
methods: info.methods,
|
|
notifications: info.notifications,
|
|
},
|
|
});
|
|
|
|
// Update balance if we got it
|
|
if (balance !== undefined) {
|
|
updateNWCBalance(balance);
|
|
}
|
|
|
|
// Update info
|
|
updateNWCInfo({
|
|
alias: info.alias,
|
|
network: info.network,
|
|
methods: info.methods,
|
|
notifications: info.notifications,
|
|
});
|
|
|
|
// Show success toast
|
|
toast.success("Wallet Connected");
|
|
|
|
// Close dialog
|
|
onOpenChange(false);
|
|
|
|
// Call onConnected callback
|
|
onConnected?.();
|
|
} catch (err) {
|
|
console.error("Wallet connection error:", err);
|
|
setError(err instanceof Error ? err.message : "Failed to connect wallet");
|
|
toast.error("Failed to connect wallet");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>Connect Wallet</DialogTitle>
|
|
<DialogDescription>
|
|
Connect to a Nostr Wallet Connect (NWC) enabled Lightning wallet
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
<p className="text-sm text-muted-foreground">
|
|
Enter your wallet connection string. You can get this from your NWC
|
|
wallet provider.
|
|
</p>
|
|
|
|
{/* Security warning */}
|
|
<div className="flex items-start gap-2 rounded-md border border-yellow-500/50 bg-yellow-500/10 p-3 text-sm">
|
|
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-yellow-600 dark:text-yellow-500" />
|
|
<div className="space-y-1">
|
|
<p className="font-medium text-yellow-900 dark:text-yellow-200">
|
|
Security Notice
|
|
</p>
|
|
<p className="text-yellow-800 dark:text-yellow-300">
|
|
Your wallet connection will be stored in browser storage. Only
|
|
connect on trusted devices.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="flex items-start gap-2 rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
|
|
<AlertCircle className="mt-0.5 size-4 shrink-0" />
|
|
<span>{error}</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-2">
|
|
<label
|
|
htmlFor="connection-string"
|
|
className="text-sm font-medium leading-none"
|
|
>
|
|
Connection String
|
|
</label>
|
|
<Input
|
|
id="connection-string"
|
|
placeholder="nostr+walletconnect://..."
|
|
value={connectionString}
|
|
onChange={(e) => setConnectionString(e.target.value)}
|
|
disabled={loading}
|
|
autoComplete="off"
|
|
/>
|
|
</div>
|
|
|
|
<Button
|
|
onClick={handleConnect}
|
|
disabled={loading || !connectionString.trim()}
|
|
className="w-full"
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<Loader2 className="mr-2 size-4 animate-spin" />
|
|
Connecting...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Wallet className="mr-2 size-4" />
|
|
Connect Wallet
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|