diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b909a43..c5992336 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed +- `BatchTip` now reports accurate outcome summaries after on-chain + confirmation instead of always showing a blanket success toast. Non-strict + batch results are parsed to show full success, partial success, or all + failed outcomes (Issue #238). + - `RecentTips` tip-back modal now provides complete dialog keyboard support: it traps `Tab`/`Shift+Tab` focus within the modal, closes on `Escape`, restores focus to the previously focused trigger on close, and supports @@ -56,6 +61,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `Escape` close with focus restoration, focus trapping, and backdrop click close behavior. +### Added (Issue #238) + +- `frontend/src/lib/batchTipResults.js` with result parsing helpers used to + summarize per-recipient outcomes from confirmed batch-tip transactions. +- `frontend/src/test/batch-tip-results.test.js` with 6 tests covering + non-strict result parsing, strict-mode fallback parsing, and final + user-facing outcome message generation. + - Four components (`Leaderboard`, `RecentTips`, `TipHistory`, `useNotifications`) each polled the same Stacks API contract-events endpoint on independent intervals, generating up to 15+ requests per diff --git a/frontend/src/components/BatchTip.jsx b/frontend/src/components/BatchTip.jsx index bfb4172f..3b0005ce 100644 --- a/frontend/src/components/BatchTip.jsx +++ b/frontend/src/components/BatchTip.jsx @@ -14,10 +14,12 @@ import { CONTRACT_ADDRESS, CONTRACT_NAME, FN_SEND_BATCH_TIPS, FN_SEND_BATCH_TIPS import { toMicroSTX, formatSTX, formatAddress } from '../lib/utils'; import { formatBalance } from '../lib/balance-utils'; import { analytics } from '../lib/analytics'; +import { summarizeBatchTipResult, buildBatchTipOutcomeMessage } from '../lib/batchTipResults'; import { useBalance } from '../hooks/useBalance'; import { useTipContext } from '../context/TipContext'; import { Users, Plus, Trash2, Send, Loader2, AlertTriangle } from 'lucide-react'; import ConfirmDialog from './ui/confirm-dialog'; +import TxStatus from './ui/tx-status'; const MAX_BATCH_SIZE = 50; const MIN_TIP_STX = 0.001; @@ -33,6 +35,7 @@ export default function BatchTip({ addToast }) { const [sending, setSending] = useState(false); const [showConfirm, setShowConfirm] = useState(false); const [errors, setErrors] = useState({}); + const [pendingTx, setPendingTx] = useState(null); const senderAddress = useMemo(() => getSenderAddress(), []); @@ -187,13 +190,12 @@ export default function BatchTip({ addToast }) { postConditionMode: PostConditionMode.Deny, onFinish: (data) => { setSending(false); - analytics.trackBatchTipConfirmed(); + setPendingTx({ txId: data.txId, totalRecipients: recipients.length, strictMode }); setRecipients([emptyRecipient()]); setErrors({}); - notifyTipSent(); addToast?.( - `Batch of ${recipients.length} tips sent! Tx: ${data.txId}`, - 'success' + `Batch transaction submitted. Waiting for confirmation... Tx: ${data.txId}`, + 'info' ); }, onCancel: () => { @@ -210,6 +212,25 @@ export default function BatchTip({ addToast }) { } }; + const handleBatchTxConfirmed = useCallback((txData) => { + analytics.trackBatchTipConfirmed(); + notifyTipSent(); + + const summary = summarizeBatchTipResult(txData, pendingTx?.totalRecipients ?? 0); + const toastType = summary.failureCount === 0 + ? 'success' + : summary.successCount > 0 + ? 'warning' + : 'error'; + + addToast?.(buildBatchTipOutcomeMessage(summary), toastType); + }, [addToast, notifyTipSent, pendingTx?.totalRecipients]); + + const handleBatchTxFailed = useCallback((reason) => { + analytics.trackBatchTipFailed(); + addToast?.(`Batch transaction failed: ${reason}`, 'error'); + }, [addToast]); + return (
@@ -407,6 +428,14 @@ export default function BatchTip({ addToast }) { )} + + {pendingTx?.txId && ( + + )}
0 ? expectedTotal : successCount; + return { + total, + successCount, + failureCount: Math.max(0, total - successCount), + parsed: true, + }; + } + + return { + total: expectedTotal, + successCount: expectedTotal, + failureCount: 0, + parsed: false, + }; +} + +export function buildBatchTipOutcomeMessage(summary) { + const total = summary?.total ?? 0; + const successCount = summary?.successCount ?? 0; + const failureCount = summary?.failureCount ?? 0; + + if (total <= 0) { + return 'Batch transaction confirmed. No tip outcomes were reported.'; + } + + if (failureCount <= 0) { + return `${successCount} of ${total} tips sent successfully`; + } + + if (successCount <= 0) { + return 'Batch transaction completed but all tips failed'; + } + + return `${successCount} of ${total} tips sent. ${failureCount} failed (see transaction details)`; +} diff --git a/frontend/src/test/batch-tip-results.test.js b/frontend/src/test/batch-tip-results.test.js new file mode 100644 index 00000000..6b72f995 --- /dev/null +++ b/frontend/src/test/batch-tip-results.test.js @@ -0,0 +1,60 @@ +import { describe, it, expect } from 'vitest'; +import { summarizeBatchTipResult, buildBatchTipOutcomeMessage } from '../lib/batchTipResults'; + +describe('summarizeBatchTipResult', () => { + it('counts per-tip ok/err outcomes from a non-strict list result', () => { + const txData = { + tx_result: { + repr: '(ok (list (ok u1) (err u108) (ok u2) (err u101)))', + }, + }; + + expect(summarizeBatchTipResult(txData, 4)).toEqual({ + total: 4, + successCount: 2, + failureCount: 2, + parsed: true, + }); + }); + + it('parses strict-mode count result', () => { + const txData = { + tx_result: { + repr: '(ok u3)', + }, + }; + + expect(summarizeBatchTipResult(txData, 5)).toEqual({ + total: 5, + successCount: 3, + failureCount: 2, + parsed: true, + }); + }); + + it('falls back to expected total when repr is missing', () => { + expect(summarizeBatchTipResult({}, 3)).toEqual({ + total: 3, + successCount: 3, + failureCount: 0, + parsed: false, + }); + }); +}); + +describe('buildBatchTipOutcomeMessage', () => { + it('returns full success message', () => { + const msg = buildBatchTipOutcomeMessage({ total: 5, successCount: 5, failureCount: 0 }); + expect(msg).toBe('5 of 5 tips sent successfully'); + }); + + it('returns partial success message', () => { + const msg = buildBatchTipOutcomeMessage({ total: 5, successCount: 3, failureCount: 2 }); + expect(msg).toBe('3 of 5 tips sent. 2 failed (see transaction details)'); + }); + + it('returns all failed message', () => { + const msg = buildBatchTipOutcomeMessage({ total: 5, successCount: 0, failureCount: 5 }); + expect(msg).toBe('Batch transaction completed but all tips failed'); + }); +});