From bb97e8dedbee741bd1db21a08dddae832cc69961 Mon Sep 17 00:00:00 2001 From: 9qeklajc Date: Wed, 25 Mar 2026 10:21:18 +0100 Subject: [PATCH] use react query --- ui/components/child-key-creator.tsx | 80 ++++++++---- .../landing/cashu-payment-workflow.tsx | 122 ++++++++---------- ui/components/landing/key-info-details.tsx | 79 +++--------- ui/hooks/use-wallet-info.ts | 38 ++++++ 4 files changed, 165 insertions(+), 154 deletions(-) create mode 100644 ui/hooks/use-wallet-info.ts diff --git a/ui/components/child-key-creator.tsx b/ui/components/child-key-creator.tsx index 4db00e2d..b6e5c324 100644 --- a/ui/components/child-key-creator.tsx +++ b/ui/components/child-key-creator.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState } from 'react'; +import { useWalletInfo } from '@/hooks/use-wallet-info'; import { WalletService } from '@/lib/api/services/wallet'; import { ApiKeyInput } from './api-key-input'; import { Button } from '@/components/ui/button'; @@ -15,17 +16,8 @@ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; -import { - Key, - Copy, - Check, - Loader2, - RotateCcw, - Plus, - Trash2, -} from 'lucide-react'; +import { Key, Copy, Check, Loader2, Plus, Trash2 } from 'lucide-react'; import { toast } from 'sonner'; -import { Badge } from '@/components/ui/badge'; import { KeyOptions } from './key-options'; interface KeyConfig { @@ -43,6 +35,14 @@ interface ChildKeyCreatorProps { costPerKeyMsats?: number; } +function formatSats(msats: number): string { + return new Intl.NumberFormat('en-US').format(Math.floor(msats / 1000)); +} + +function formatMsats(msats: number): string { + return new Intl.NumberFormat('en-US').format(msats); +} + export function ChildKeyCreator({ baseUrl, apiKey: propApiKey, @@ -62,14 +62,18 @@ export function ChildKeyCreator({ }, ]); const [childKeyToCheck, setChildKeyToCheck] = useState(''); - const [checking, setChecking] = useState(false); - const [keyStatus, setKeyStatus] = useState<{ - total_spent: number; - balance_limit: number | null; - validity_date: number | null; - is_expired: boolean; - is_drained: boolean; - } | null>(null); + + const activeApiKey = propApiKey ?? internalApiKey; + const { data: walletInfo, refetch: refetchWalletInfo } = useWalletInfo( + baseUrl ?? '', + activeApiKey + ); + + const handleApiKeyChange = (val: string) => { + setInternalApiKey(val); + onApiKeyChange?.(val); + }; + const [newKeys, setNewKeys] = useState([]); const [resultInfo, setResultInfo] = useState<{ cost_msats: number; @@ -77,13 +81,6 @@ export function ChildKeyCreator({ } | null>(null); const [copiedKey, setCopiedKey] = useState(null); - const activeApiKey = propApiKey ?? internalApiKey; - - const handleApiKeyChange = (val: string) => { - setInternalApiKey(val); - onApiKeyChange?.(val); - }; - const addConfig = () => { setConfigs([ ...configs, @@ -269,6 +266,39 @@ export function ChildKeyCreator({ + {walletInfo && ( +
+
+ + Spendable Balance + + + {formatSats(walletInfo.balanceMsats)} sats + +
+
+ + Total Requests + + + {walletInfo.totalRequests} + +
+
+ + Total Spent + +
+

+ {formatSats(walletInfo.totalSpent)} sats +

+

+ {formatMsats(walletInfo.totalSpent)} msats +

+
+
+
+ )} )} diff --git a/ui/components/landing/cashu-payment-workflow.tsx b/ui/components/landing/cashu-payment-workflow.tsx index 5608833f..ccf03460 100644 --- a/ui/components/landing/cashu-payment-workflow.tsx +++ b/ui/components/landing/cashu-payment-workflow.tsx @@ -10,9 +10,9 @@ import { Textarea } from '@/components/ui/textarea'; import { Input } from '@/components/ui/input'; import { Separator } from '@/components/ui/separator'; import { KeyOptions } from '@/components/key-options'; -import { WalletBalanceStats } from './wallet-balance-stats'; import { ApiKeyInput } from '../api-key-input'; import type { ChildKeyInfo, WalletSnapshot } from './key-info-details'; +import { useWalletInfo } from '@/hooks/use-wallet-info'; export type RefundReceipt = { token?: string; @@ -31,68 +31,18 @@ interface CashuPaymentWorkflowProps { onRefundComplete?: (receipt: RefundReceipt) => void; } -async function fetchWalletInfo( - baseUrl: string, - apiKey: string -): Promise { - const response = await fetch(`${baseUrl}/v1/balance/info`, { - cache: 'no-store', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${apiKey}`, - }, - }); - - if (!response.ok) { - const errorText = await response.text(); - let errorMessage = errorText || 'Unable to load wallet info'; - try { - const parsed = JSON.parse(errorText); - // The error structure seems to be { detail: { error: { message: ... } } } - errorMessage = - parsed.detail?.error?.message || - (typeof parsed.detail === 'string' ? parsed.detail : errorMessage); - } catch {} - throw new Error(errorMessage); - } - - const payload = (await response.json()) as { - api_key: string; - balance: number; - reserved?: number; - is_child: boolean; - parent_key: string | null; - total_requests: number; - total_spent: number; - balance_limit: number | null; - balance_limit_reset: string | null; - validity_date: number | null; - child_keys?: ChildKeyInfo[]; - }; - - return { - apiKey: payload.api_key || apiKey, - balanceMsats: payload.balance ?? 0, - reservedMsats: payload.reserved ?? 0, - isChild: payload.is_child, - parentKey: payload.parent_key, - totalRequests: payload.total_requests, - totalSpent: payload.total_spent, - balanceLimit: payload.balance_limit, - balanceLimitReset: payload.balance_limit_reset, - validityDate: payload.validity_date, - childKeys: payload.child_keys, - }; -} - function formatSats(msats: number): string { return new Intl.NumberFormat('en-US').format(Math.floor(msats / 1000)); } +function formatMsats(msats: number): string { + return new Intl.NumberFormat('en-US').format(msats); +} + export function CashuPaymentWorkflow({ baseUrl, apiKey = '', - walletInfo = null, + walletInfo: propWalletInfo = null, onApiKeyCreated, onWalletInfoUpdated, }: CashuPaymentWorkflowProps): JSX.Element { @@ -101,7 +51,6 @@ export function CashuPaymentWorkflow({ const [apiKeyInput, setApiKeyInput] = useState(apiKey); const [isCreatingKey, setIsCreatingKey] = useState(false); const [isTopupLoading, setIsTopupLoading] = useState(false); - const [isSyncingBalance, setIsSyncingBalance] = useState(false); const [hasInteractedManage, setHasInteractedManage] = useState(false); const [hasInteractedTopup, setHasInteractedTopup] = useState(false); const [balanceLimit, setBalanceLimit] = useState(''); @@ -111,6 +60,13 @@ export function CashuPaymentWorkflow({ const activeApiKey = apiKeyInput.trim(); + const { + data: queryWalletInfo, + refetch, + isFetching, + } = useWalletInfo(baseUrl, activeApiKey); + const walletInfo = propWalletInfo ?? queryWalletInfo ?? null; + const handleCopy = useCallback(async (value: string): Promise => { if (!value) { return; @@ -211,11 +167,9 @@ export function CashuPaymentWorkflow({ return; } - setIsSyncingBalance(true); setError(null); try { - const snapshot = await fetchWalletInfo(baseUrl, activeApiKey); - onWalletInfoUpdated?.(snapshot); + await refetch(); toast.success('Balance synced'); } catch (error) { console.error(error); @@ -223,10 +177,8 @@ export function CashuPaymentWorkflow({ error instanceof Error ? error.message : 'Failed to sync balance'; setError(message); toast.error(message); - } finally { - setIsSyncingBalance(false); } - }, [activeApiKey, baseUrl, onWalletInfoUpdated]); + }, [activeApiKey, refetch]); const handleTopup = useCallback(async (): Promise => { if (!activeApiKey) { @@ -255,15 +207,14 @@ export function CashuPaymentWorkflow({ const payload = (await response.json()) as { msats: number }; toast.success(`Added ${formatSats(payload.msats)} sats`); setTopupToken(''); - const snapshot = await fetchWalletInfo(baseUrl, activeApiKey); - onApiKeyCreated?.(snapshot.apiKey, snapshot); + await refetch(); } catch (error) { console.error(error); toast.error(error instanceof Error ? error.message : 'Top-up failed'); } finally { setIsTopupLoading(false); } - }, [activeApiKey, baseUrl, topupToken, onApiKeyCreated]); + }, [activeApiKey, baseUrl, topupToken, refetch]); const handleApiKeyChange = useCallback( (newKey: string) => { @@ -363,13 +314,48 @@ export function CashuPaymentWorkflow({ size='sm' className='gap-1' onClick={handleSyncBalance} - disabled={isSyncingBalance || !activeApiKey} + disabled={isFetching || !activeApiKey} > - + Sync + {walletInfo && ( +
+
+ + Spendable Balance + + + {formatSats(walletInfo.balanceMsats)} sats + +
+
+ + Total Requests + + + {walletInfo.totalRequests} + +
+
+ + Total Spent + +
+

+ {formatSats(walletInfo.totalSpent)} sats +

+

+ {formatMsats(walletInfo.totalSpent)} msats +

+
+
+
+ )} {error && ( Error diff --git a/ui/components/landing/key-info-details.tsx b/ui/components/landing/key-info-details.tsx index 22be49b0..4375d6a3 100644 --- a/ui/components/landing/key-info-details.tsx +++ b/ui/components/landing/key-info-details.tsx @@ -1,7 +1,6 @@ 'use client'; -import { type JSX, useState, useCallback, useEffect } from 'react'; -import { Copy, RefreshCcw, RotateCcw, Trash2 } from 'lucide-react'; +import { useWalletInfo } from '@/hooks/use-wallet-info'; import type { RefundReceipt } from './cashu-payment-workflow'; import { toast } from 'sonner'; import { ApiKeyInput } from '../api-key-input'; @@ -16,6 +15,8 @@ import { import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { WalletService } from '@/lib/api/services/wallet'; +import { useState, useCallback, useEffect } from 'react'; +import { Copy, RefreshCcw, RotateCcw, Trash2 } from 'lucide-react'; export type ChildKeyInfo = { api_key: string; @@ -52,76 +53,33 @@ interface KeyInfoDetailsProps { export function KeyInfoDetails({ baseUrl, apiKey = '', - walletInfo = null, + walletInfo: propWalletInfo = null, onApiKeyChanged, onWalletInfoUpdated, onRefundComplete, }: KeyInfoDetailsProps): JSX.Element { const [apiKeyInput, setApiKeyInput] = useState(apiKey); - const [isRefreshing, setIsRefreshing] = useState(false); const [isResetting, setIsResetting] = useState(null); const [isRefunding, setIsRefunding] = useState(false); const [error, setError] = useState(null); + const { + data: queryWalletInfo, + refetch, + isFetching, + } = useWalletInfo(baseUrl, apiKeyInput); + const walletInfo = propWalletInfo ?? queryWalletInfo ?? null; + // Sync internal state with props if they change useEffect(() => { setApiKeyInput(apiKey); }, [apiKey]); - const fetchDetails = useCallback( - async (keyToFetch: string) => { - setIsRefreshing(true); - setError(null); - try { - const response = await fetch(`${baseUrl}/v1/balance/info`, { - headers: { Authorization: `Bearer ${keyToFetch}` }, - }); - if (!response.ok) { - const errorText = await response.text(); - let errorMessage = errorText || 'Failed to fetch key info'; - try { - const parsed = JSON.parse(errorText); - // The error structure seems to be { detail: { error: { message: ... } } } - errorMessage = - parsed.detail?.error?.message || - (typeof parsed.detail === 'string' - ? parsed.detail - : errorMessage); - } catch {} - throw new Error(errorMessage); - } - const payload = await response.json(); - const snapshot: WalletSnapshot = { - apiKey: payload.api_key || keyToFetch, - balanceMsats: payload.balance ?? 0, - reservedMsats: payload.reserved ?? 0, - isChild: payload.is_child, - parentKey: payload.parent_key, - totalRequests: payload.total_requests, - totalSpent: payload.total_spent, - balanceLimit: payload.balance_limit, - balanceLimitReset: payload.balance_limit_reset, - validityDate: payload.validity_date, - childKeys: payload.child_keys, - }; - onWalletInfoUpdated?.(snapshot); - toast.success('Key details synced'); - } catch (error) { - const message = - error instanceof Error ? error.message : 'Failed to fetch details'; - setError(message); - } finally { - setIsRefreshing(false); - } - }, - [baseUrl, onWalletInfoUpdated] - ); - const handleRefresh = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!apiKeyInput) return; - await fetchDetails(apiKeyInput); + await refetch(); }; const handleKeyChange = (newKey: string) => { @@ -146,7 +104,7 @@ export function KeyInfoDetails({ try { await WalletService.resetChildKeySpent(baseUrl, apiKeyInput, childKey); toast.success('Child key spent reset'); - await fetchDetails(apiKeyInput); + await refetch(); } catch (error) { toast.error( error instanceof Error ? error.message : 'Failed to reset child key' @@ -176,16 +134,15 @@ export function KeyInfoDetails({ } const receipt = (await response.json()) as RefundReceipt; onRefundComplete?.(receipt); - // Removed onWalletInfoUpdated?.(null); // Prevents card disappearance toast.success('Refund completed'); - await fetchDetails(apiKeyInput); // Refresh to show burned status + await refetch(); } catch (error) { console.error(error); toast.error(error instanceof Error ? error.message : 'Refund failed'); } finally { setIsRefunding(false); } - }, [apiKeyInput, baseUrl, onRefundComplete, onWalletInfoUpdated]); + }, [apiKeyInput, baseUrl, onRefundComplete, refetch]); const formatSats = (msats: number) => new Intl.NumberFormat('en-US').format(Math.floor(msats / 1000)); @@ -220,13 +177,13 @@ export function KeyInfoDetails({ size='sm' className='min-w-[80px] gap-1' onClick={handleRefresh} - disabled={isRefreshing || !apiKeyInput} + disabled={isFetching || !apiKeyInput} type='button' > - {isRefreshing ? 'Syncing...' : 'Sync'} + {isFetching ? 'Syncing...' : 'Sync'} diff --git a/ui/hooks/use-wallet-info.ts b/ui/hooks/use-wallet-info.ts new file mode 100644 index 00000000..fd7d159e --- /dev/null +++ b/ui/hooks/use-wallet-info.ts @@ -0,0 +1,38 @@ +import { useQuery } from '@tanstack/react-query'; +import type { WalletSnapshot } from '@/components/landing/key-info-details'; + +export function useWalletInfo(baseUrl: string, apiKey: string) { + return useQuery({ + queryKey: ['walletInfo', baseUrl, apiKey], + queryFn: async (): Promise => { + const response = await fetch(`${baseUrl}/v1/balance/info`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || 'Unable to load wallet info'); + } + + const payload = await response.json(); + return { + apiKey: payload.api_key || apiKey, + balanceMsats: payload.balance ?? 0, + reservedMsats: payload.reserved ?? 0, + isChild: payload.is_child, + parentKey: payload.parent_key, + totalRequests: payload.total_requests, + totalSpent: payload.total_spent, + balanceLimit: payload.balance_limit, + balanceLimitReset: payload.balance_limit_reset, + validityDate: payload.validity_date, + childKeys: payload.child_keys, + }; + }, + enabled: !!baseUrl && !!apiKey, + staleTime: 5000, // Consider data stale after 5 seconds + }); +}