From 4ee654331fe012fc53a8d8ba8e9af7ccb77a519f Mon Sep 17 00:00:00 2001 From: 9qeklajc Date: Mon, 23 Mar 2026 22:01:54 +0100 Subject: [PATCH] fix refunding not displayed & clean up --- ui/components/child-key-creator.tsx | 111 ++++-------------- ui/components/landing/api-key-manager.tsx | 2 +- .../landing/cashu-payment-workflow.tsx | 91 ++++---------- ui/components/landing/cheat-sheet.tsx | 26 ++-- ui/components/landing/key-info-details.tsx | 68 +++++++++-- 5 files changed, 108 insertions(+), 190 deletions(-) diff --git a/ui/components/child-key-creator.tsx b/ui/components/child-key-creator.tsx index 1c67b442..460108b9 100644 --- a/ui/components/child-key-creator.tsx +++ b/ui/components/child-key-creator.tsx @@ -50,6 +50,7 @@ export function ChildKeyCreator({ }: ChildKeyCreatorProps) { const [internalApiKey, setInternalApiKey] = useState(''); const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); const [configs, setConfigs] = useState([ { id: crypto.randomUUID(), @@ -112,6 +113,7 @@ export function ChildKeyCreator({ } setLoading(true); + setError(null); try { let allNewKeys: string[] = []; let totalCost = 0; @@ -152,9 +154,14 @@ export function ChildKeyCreator({ ); } catch (error) { console.error('Failed to create child key:', error); - toast.error( - error instanceof Error ? error.message : 'Failed to create child key' - ); + let errorMessage = + error instanceof Error ? error.message : 'Failed to create child key'; + try { + const parsed = JSON.parse(errorMessage); + errorMessage = parsed.detail || parsed.message || errorMessage; + } catch {} + setError(errorMessage); + toast.error(errorMessage); } finally { setLoading(false); } @@ -362,11 +369,18 @@ export function ChildKeyCreator({ )} - -

- Each key creation has a small one-time fee. -

+ {error && ( + + Error + {error} + + )} + +

+ Each key creation has a small one-time fee. +

+ {newKeys.length > 0 && (
@@ -458,89 +472,6 @@ export function ChildKeyCreator({
- - - - Check Child Key Status - - View the current spending, limit, and expiration status of any child - key. - - - -
-
- - setChildKeyToCheck(e.target.value)} - placeholder='sk-...' - className='font-mono text-sm' - /> -
- - - {keyStatus && ( -
-
- Total Spent: - - {keyStatus.total_spent} mSats - -
- {keyStatus.balance_limit !== null && ( -
- Limit: - - {keyStatus.balance_limit} mSats - -
- )} - {keyStatus.validity_date !== null && ( -
- Expires: - - {new Date( - keyStatus.validity_date * 1000 - ).toLocaleDateString()} - -
- )} -
- {keyStatus.is_drained && ( - Drained - )} - {keyStatus.is_expired && ( - Expired - )} - {!keyStatus.is_drained && !keyStatus.is_expired && ( - Active - )} -
-
- )} -
-
-
); } diff --git a/ui/components/landing/api-key-manager.tsx b/ui/components/landing/api-key-manager.tsx index e8c2238e..7cc4aafb 100644 --- a/ui/components/landing/api-key-manager.tsx +++ b/ui/components/landing/api-key-manager.tsx @@ -239,7 +239,7 @@ export function ApiKeyManager({ className='gap-2' > - {isRefunding ? 'Processing...' : 'Refund & Delete Key'} + {isRefunding ? 'Processing...' : 'Refund Key'} Burns the key and returns a fresh Cashu token. diff --git a/ui/components/landing/cashu-payment-workflow.tsx b/ui/components/landing/cashu-payment-workflow.tsx index d2e80c47..7af3e231 100644 --- a/ui/components/landing/cashu-payment-workflow.tsx +++ b/ui/components/landing/cashu-payment-workflow.tsx @@ -2,6 +2,7 @@ import { type JSX, useCallback, useState } from 'react'; import { Copy, RefreshCcw, Trash2 } from 'lucide-react'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { toast } from 'sonner'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -43,7 +44,15 @@ async function fetchWalletInfo( if (!response.ok) { const errorText = await response.text(); - throw new Error(errorText || 'Unable to load wallet info'); + 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 { @@ -84,22 +93,20 @@ export function CashuPaymentWorkflow({ apiKey = '', walletInfo = null, onApiKeyCreated, - onApiKeyChanged, onWalletInfoUpdated, - onRefundComplete, }: CashuPaymentWorkflowProps): JSX.Element { const [initialToken, setInitialToken] = useState(''); const [topupToken, setTopupToken] = useState(''); const [apiKeyInput, setApiKeyInput] = useState(apiKey); const [isCreatingKey, setIsCreatingKey] = useState(false); const [isTopupLoading, setIsTopupLoading] = useState(false); - const [isRefunding, setIsRefunding] = useState(false); const [isSyncingBalance, setIsSyncingBalance] = useState(false); const [hasInteractedManage, setHasInteractedManage] = useState(false); const [hasInteractedTopup, setHasInteractedTopup] = useState(false); const [balanceLimit, setBalanceLimit] = useState(''); const [balanceLimitReset, setBalanceLimitReset] = useState(''); const [validityDate, setValidityDate] = useState(''); + const [error, setError] = useState(null); const activeApiKey = apiKeyInput.trim(); @@ -204,15 +211,17 @@ export function CashuPaymentWorkflow({ } setIsSyncingBalance(true); + setError(null); try { const snapshot = await fetchWalletInfo(baseUrl, activeApiKey); onWalletInfoUpdated?.(snapshot); toast.success('Balance synced'); } catch (error) { console.error(error); - toast.error( - error instanceof Error ? error.message : 'Failed to sync balance' - ); + const message = + error instanceof Error ? error.message : 'Failed to sync balance'; + setError(message); + toast.error(message); } finally { setIsSyncingBalance(false); } @@ -255,46 +264,14 @@ export function CashuPaymentWorkflow({ } }, [activeApiKey, baseUrl, topupToken, onApiKeyCreated]); - const handleRefund = useCallback(async (): Promise => { - if (!activeApiKey) { - toast.error('Paste an API key first'); - return; - } - - setIsRefunding(true); - try { - const response = await fetch(`${baseUrl}/v1/balance/refund`, { - method: 'POST', - headers: { - Authorization: `Bearer ${activeApiKey}`, - }, - }); - if (!response.ok) { - const errorText = await response.text(); - throw new Error(errorText || 'Refund failed'); - } - const payload = (await response.json()) as RefundReceipt; - onRefundComplete?.(payload); - onWalletInfoUpdated?.(null); - setApiKeyInput(''); - toast.success('Refund requested'); - } catch (error) { - console.error(error); - toast.error(error instanceof Error ? error.message : 'Refund failed'); - } finally { - setIsRefunding(false); - } - }, [activeApiKey, baseUrl, onRefundComplete, onWalletInfoUpdated]); - const handleApiKeyChange = useCallback( (newKey: string) => { setApiKeyInput(newKey); - onApiKeyChanged?.(newKey); if (newKey !== apiKey) { onWalletInfoUpdated?.(null); } }, - [apiKey, onApiKeyChanged, onWalletInfoUpdated] + [apiKey] ); const showManageDetails = hasInteractedManage || Boolean(walletInfo); @@ -376,12 +353,12 @@ export function CashuPaymentWorkflow({ + - - Burns the key and returns a fresh Cashu token. - - - ); diff --git a/ui/components/landing/cheat-sheet.tsx b/ui/components/landing/cheat-sheet.tsx index 7004ec98..f5fd803c 100644 --- a/ui/components/landing/cheat-sheet.tsx +++ b/ui/components/landing/cheat-sheet.tsx @@ -18,7 +18,6 @@ import { type RefundReceipt, } from './cashu-payment-workflow'; import { LightningPaymentWorkflow } from './lightning-payment-workflow'; -import { ApiKeyManager } from './api-key-manager'; import { KeyInfoDetails, type WalletSnapshot } from './key-info-details'; import { ChildKeyCreator } from '@/components/child-key-creator'; @@ -135,8 +134,8 @@ export function CheatSheet(): JSX.Element { const handleRefundComplete = useCallback((receipt: RefundReceipt) => { setRefundReceipt(receipt); - setWalletInfo(null); - setApiKeyInput(''); + // setWalletInfo(null); // Keep info + // setApiKeyInput(''); // Keep input }, []); const handleRefreshInfo = useCallback(async (): Promise => { @@ -391,12 +390,11 @@ export function CheatSheet(): JSX.Element { - + Cashu Lightning - Manage Keys - Key Details Child Keys + Key Management @@ -413,8 +411,8 @@ export function CheatSheet(): JSX.Element { /> - - + @@ -454,16 +452,6 @@ export function CheatSheet(): JSX.Element { )} - - - - void; onWalletInfoUpdated?: (walletInfo: WalletSnapshot | null) => void; + onRefundComplete?: (receipt: RefundReceipt) => void; } export function KeyInfoDetails({ @@ -52,10 +54,12 @@ export function KeyInfoDetails({ walletInfo = 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); // Sync internal state with props if they change useEffect(() => { @@ -135,6 +139,37 @@ export function KeyInfoDetails({ } }; + const handleRefund = useCallback(async (): Promise => { + if (!apiKeyInput) { + toast.error('Paste an API key first'); + return; + } + + setIsRefunding(true); + try { + const response = await fetch(`${baseUrl}/v1/balance/refund`, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKeyInput}`, + }, + }); + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || 'Refund failed'); + } + 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 + } catch (error) { + console.error(error); + toast.error(error instanceof Error ? error.message : 'Refund failed'); + } finally { + setIsRefunding(false); + } + }, [apiKeyInput, baseUrl, onRefundComplete, onWalletInfoUpdated]); + const formatSats = (msats: number) => new Intl.NumberFormat('en-US').format(Math.floor(msats / 1000)); const formatMsats = (msats: number) => @@ -163,7 +198,6 @@ export function KeyInfoDetails({ @@ -228,6 +262,14 @@ export function KeyInfoDetails({ {formatDate(walletInfo.validityDate)} + + + + + + Infos + +
Spendable Balance @@ -236,14 +278,6 @@ export function KeyInfoDetails({ {formatSats(walletInfo.balanceMsats)} sats
-
-
- - - - Consumption - -
Total Requests @@ -390,7 +424,7 @@ export function KeyInfoDetails({ )} -
+
+
)}