From d1fc0dc1030eef9c2da554384fc1831b81b52c70 Mon Sep 17 00:00:00 2001 From: NteinPrecious Date: Fri, 16 Oct 2026 09:20:16 +0100 Subject: [PATCH] fix(dashboard): refresh balances on incoming payments --- .../features/dashboard/services/index.ts | 36 +++++++++++++++++-- .../features/dashboard/state/index.ts | 8 +++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/ignition-pay-frontend/features/dashboard/services/index.ts b/ignition-pay-frontend/features/dashboard/services/index.ts index f5f48493..080586b2 100644 --- a/ignition-pay-frontend/features/dashboard/services/index.ts +++ b/ignition-pay-frontend/features/dashboard/services/index.ts @@ -185,26 +185,56 @@ export function subscribeToWalletStream( address: string, handlers: WalletStreamHandlers, ): (() => void) | null { - const streamUrl = process.env.NEXT_PUBLIC_WALLET_STREAM_URL - if (!streamUrl || typeof EventSource === 'undefined') return null + if (typeof EventSource === 'undefined') return null + + const backendUrl = process.env.NEXT_PUBLIC_WALLET_STREAM_URL + const horizonUrl = process.env.NEXT_PUBLIC_HORIZON_URL + + // Prefer the backend notification stream. Without one, fall back to + // Horizon's own payments stream for the account (server-sent events), so an + // incoming payment still refreshes balances immediately instead of waiting + // for the next poll. + let streamUrl: string | null = null + if (backendUrl) { + streamUrl = `${backendUrl}?address=${encodeURIComponent(address)}` + } else if (horizonUrl && /^G[A-Z2-7]{55}$/.test(address)) { + streamUrl = `${horizonUrl.replace(/\/$/, '')}/accounts/${address}/payments?cursor=now` + } + if (!streamUrl) return null - const source = new EventSource(`${streamUrl}?address=${encodeURIComponent(address)}`) + const source = new EventSource(streamUrl) const handleMessage = () => handlers.onBalanceChange() const handleError = () => handlers.onError?.() source.addEventListener('balance', handleMessage) source.addEventListener('payment', handleMessage) + // Horizon sends unnamed `message` events for each new payment operation. + source.addEventListener('message', handleMessage) source.addEventListener('error', handleError) return () => { source.removeEventListener('balance', handleMessage) source.removeEventListener('payment', handleMessage) + source.removeEventListener('message', handleMessage) source.removeEventListener('error', handleError) source.close() } } +/** Window event that asks every mounted `useWalletBalances` to reload now. */ +export const WALLET_BALANCES_INVALIDATED_EVENT = 'walletBalancesInvalidated' + +/** + * Invalidate cached dashboard balances. Call after any flow that is known to + * change the balance (e.g. a completed receive via QR scan or deep link). + */ +export function invalidateWalletBalances(): void { + if (typeof window !== 'undefined') { + window.dispatchEvent(new Event(WALLET_BALANCES_INVALIDATED_EVENT)) + } +} + /** * Fetches live price history and 24h changes for an asset from Stellar DEX (Horizon Trade Aggregations). * Falls back to dynamic ticker estimation if network is unavailable. diff --git a/ignition-pay-frontend/features/dashboard/state/index.ts b/ignition-pay-frontend/features/dashboard/state/index.ts index b0fdfc56..aea8aa43 100644 --- a/ignition-pay-frontend/features/dashboard/state/index.ts +++ b/ignition-pay-frontend/features/dashboard/state/index.ts @@ -12,6 +12,7 @@ import { type QuickStatsData, isLiveDataConfigured, subscribeToWalletStream, + WALLET_BALANCES_INVALIDATED_EVENT, resolveWalletAddress, } from '@/features/dashboard/services' import { ErrorMessage, ErrorCode } from '@/lib/constants' @@ -125,6 +126,13 @@ export function useWalletBalances(addressProp?: string): WalletBalancesState { } }, [address, load]) + // Explicit invalidation (e.g. after a completed receive) reloads immediately. + useEffect(() => { + const handleInvalidated = () => void load() + window.addEventListener(WALLET_BALANCES_INVALIDATED_EVENT, handleInvalidated) + return () => window.removeEventListener(WALLET_BALANCES_INVALIDATED_EVENT, handleInvalidated) + }, [load]) + // A wallet network switch makes the current balances stale, so drop them and // reload rather than showing the previous network until the next poll. useEffect(() => {