From 02c7501aa7c516affc8767901d7cf082063c806e Mon Sep 17 00:00:00 2001 From: Hugo Montenegro Date: Wed, 22 Oct 2025 19:58:05 -0300 Subject: [PATCH 1/2] fixes for prod release --- src/app/(mobile-ui)/history/page.tsx | 17 +++- src/app/(mobile-ui)/qr-pay/page.tsx | 18 ++-- src/components/Home/HomeHistory.tsx | 101 +++++++++---------- src/components/Payment/PaymentForm/index.tsx | 15 ++- 4 files changed, 86 insertions(+), 65 deletions(-) diff --git a/src/app/(mobile-ui)/history/page.tsx b/src/app/(mobile-ui)/history/page.tsx index 9b4139385..3f7b54f7d 100644 --- a/src/app/(mobile-ui)/history/page.tsx +++ b/src/app/(mobile-ui)/history/page.tsx @@ -19,6 +19,7 @@ import { useWebSocket } from '@/hooks/useWebSocket' import { TRANSACTIONS } from '@/constants/query.consts' import type { HistoryResponse } from '@/hooks/useTransactionHistory' import { AccountType } from '@/interfaces' +import { completeHistoryEntry } from '@/utils/history.utils' /** * displays the user's transaction history with infinite scrolling and date grouping. @@ -44,20 +45,26 @@ const HistoryPage = () => { // Real-time updates via WebSocket useWebSocket({ username: user?.user.username ?? undefined, - onHistoryEntry: (newEntry) => { + onHistoryEntry: async (newEntry) => { console.log('[History] New transaction received via WebSocket:', newEntry) - // Update TanStack Query cache with new transaction + // Process the entry through completeHistoryEntry to format amounts and add computed fields + // This ensures WebSocket entries match the format of API-fetched entries + const completedEntry = await completeHistoryEntry(newEntry) + + // Update TanStack Query cache with processed transaction queryClient.setQueryData>( [TRANSACTIONS, 'infinite', { limit: 20 }], (oldData) => { if (!oldData) return oldData // Check if entry exists on ANY page to prevent duplicates - const existsAnywhere = oldData.pages.some((p) => p.entries.some((e) => e.uuid === newEntry.uuid)) + const existsAnywhere = oldData.pages.some((p) => + p.entries.some((e) => e.uuid === completedEntry.uuid) + ) if (existsAnywhere) { - console.log('[History] Duplicate transaction ignored:', newEntry.uuid) + console.log('[History] Duplicate transaction ignored:', completedEntry.uuid) return oldData } @@ -68,7 +75,7 @@ const HistoryPage = () => { if (index === 0) { return { ...page, - entries: [newEntry, ...page.entries], + entries: [completedEntry, ...page.entries], } } return page diff --git a/src/app/(mobile-ui)/qr-pay/page.tsx b/src/app/(mobile-ui)/qr-pay/page.tsx index 60fb24f84..f07bac789 100644 --- a/src/app/(mobile-ui)/qr-pay/page.tsx +++ b/src/app/(mobile-ui)/qr-pay/page.tsx @@ -42,6 +42,7 @@ import { STAR_STRAIGHT_ICON } from '@/assets' import { useAuth } from '@/context/authContext' import { useWebSocket } from '@/hooks/useWebSocket' import type { HistoryEntry } from '@/hooks/useTransactionHistory' +import { completeHistoryEntry } from '@/utils/history.utils' const MAX_QR_PAYMENT_AMOUNT = '2000' @@ -138,7 +139,7 @@ export default function QRPayPage() { } const handleSimpleFiStatusUpdate = useCallback( - (entry: HistoryEntry) => { + async (entry: HistoryEntry) => { if (!pendingSimpleFiPaymentId || entry.uuid !== pendingSimpleFiPaymentId) { return } @@ -149,16 +150,19 @@ export default function QRPayPage() { console.log('[SimpleFi WebSocket] Received status update:', entry.status) + // Process entry through completeHistoryEntry to format amounts correctly + const completedEntry = await completeHistoryEntry(entry) + setIsWaitingForWebSocket(false) setPendingSimpleFiPaymentId(null) - switch (entry.status) { + switch (completedEntry.status) { case 'approved': setSimpleFiPayment({ - id: entry.uuid, - usdAmount: entry.amount, - currency: entry.currency!.code, - currencyAmount: entry.currency!.amount, + id: completedEntry.uuid, + usdAmount: completedEntry.extraData?.usdAmount || completedEntry.amount, + currency: completedEntry.currency!.code, + currencyAmount: completedEntry.currency!.amount, price: simpleFiPayment!.price, address: simpleFiPayment!.address, }) @@ -175,7 +179,7 @@ export default function QRPayPage() { break default: - console.log('[SimpleFi WebSocket] Unknown status:', entry.status) + console.log('[SimpleFi WebSocket] Unknown status:', completedEntry.status) } }, [pendingSimpleFiPaymentId, simpleFiPayment, setLoadingState] diff --git a/src/components/Home/HomeHistory.tsx b/src/components/Home/HomeHistory.tsx index 9c3f0bfc4..f48668577 100644 --- a/src/components/Home/HomeHistory.tsx +++ b/src/components/Home/HomeHistory.tsx @@ -17,6 +17,7 @@ import { KycStatusItem } from '../Kyc/KycStatusItem' import { isKycStatusItem, type KycHistoryEntry } from '@/hooks/useBridgeKycFlow' import { useWallet } from '@/hooks/wallet/useWallet' import { useUserInteractions } from '@/hooks/useUserInteractions' +import { completeHistoryEntry } from '@/utils/history.utils' /** * component to display a preview of the most recent transactions on the home page. @@ -81,70 +82,66 @@ const HomeHistory = ({ isPublic = false, username }: { isPublic?: boolean; usern useEffect(() => { if (!isLoading && historyData?.entries) { - // Start with the fetched entries - const entries: Array = [...historyData.entries] + // Process entries asynchronously to handle completeHistoryEntry + const processEntries = async () => { + // Start with the fetched entries + const entries: Array = [...historyData.entries] - // process websocket entries: update existing or add new ones - // Sort by timestamp ascending to process oldest entries first - const sortedWsEntries = [...wsHistoryEntries].sort( - (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() - ) - sortedWsEntries.forEach((wsEntry) => { - const existingIndex = entries.findIndex((entry) => entry.uuid === wsEntry.uuid) + // process websocket entries: update existing or add new ones + // Sort by timestamp ascending to process oldest entries first + const sortedWsEntries = [...wsHistoryEntries].sort( + (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() + ) - if (existingIndex !== -1) { - // update existing entry with latest websocket data - if (wsEntry.extraData) { - wsEntry.extraData.usdAmount = wsEntry.amount.toString() - } else { - wsEntry.extraData = { usdAmount: wsEntry.amount.toString() } - } - wsEntry.extraData.link = `${BASE_URL}/${wsEntry.recipientAccount.username || wsEntry.recipientAccount.identifier}?chargeId=${wsEntry.uuid}` - entries[existingIndex] = wsEntry - } else { - // add new entry if it doesn't exist - if (wsEntry.extraData) { - wsEntry.extraData.usdAmount = wsEntry.amount.toString() + // Process WebSocket entries through completeHistoryEntry to format amounts correctly + for (const wsEntry of sortedWsEntries) { + const completedEntry = await completeHistoryEntry(wsEntry) + const existingIndex = entries.findIndex((entry) => entry.uuid === completedEntry.uuid) + + if (existingIndex !== -1) { + // update existing entry with latest websocket data + entries[existingIndex] = completedEntry } else { - wsEntry.extraData = { usdAmount: wsEntry.amount.toString() } + // add new entry if it doesn't exist + entries.push(completedEntry) } - wsEntry.extraData.link = `${BASE_URL}/${wsEntry.recipientAccount.username || wsEntry.recipientAccount.identifier}?chargeId=${wsEntry.uuid}` - entries.push(wsEntry) } - }) - // Add KYC status item if applicable and not on a public page - // and the user is viewing their own history - if (isSameUser && !isPublic) { - if (user?.user?.bridgeKycStatus && user.user.bridgeKycStatus !== 'not_started') { - entries.push({ - isKyc: true, - timestamp: user.user.bridgeKycStartedAt ?? new Date(0).toISOString(), - uuid: 'bridge-kyc-status-item', - bridgeKycStatus: user.user.bridgeKycStatus, + // Add KYC status item if applicable and not on a public page + // and the user is viewing their own history + if (isSameUser && !isPublic) { + if (user?.user?.bridgeKycStatus && user.user.bridgeKycStatus !== 'not_started') { + entries.push({ + isKyc: true, + timestamp: user.user.bridgeKycStartedAt ?? new Date(0).toISOString(), + uuid: 'bridge-kyc-status-item', + bridgeKycStatus: user.user.bridgeKycStatus, + }) + } + user?.user.kycVerifications?.forEach((verification) => { + entries.push({ + isKyc: true, + timestamp: verification.approvedAt ?? new Date(0).toISOString(), + uuid: verification.providerUserId ?? `${verification.provider}-${verification.mantecaGeo}`, + verification, + }) }) } - user?.user.kycVerifications?.forEach((verification) => { - entries.push({ - isKyc: true, - timestamp: verification.approvedAt ?? new Date(0).toISOString(), - uuid: verification.providerUserId ?? `${verification.provider}-${verification.mantecaGeo}`, - verification, - }) + + // Sort entries by date in descending order + entries.sort((a, b) => { + const dateA = new Date(a.timestamp || 0).getTime() + const dateB = new Date(b.timestamp || 0).getTime() + return dateB - dateA }) - } - // Sort entries by date in descending order - entries.sort((a, b) => { - const dateA = new Date(a.timestamp || 0).getTime() - const dateB = new Date(b.timestamp || 0).getTime() - return dateB - dateA - }) + // Limit to the most recent entries + setCombinedEntries(entries.slice(0, isPublic ? 20 : 5)) + } - // Limit to the most recent entries - setCombinedEntries(entries.slice(0, isPublic ? 20 : 5)) + processEntries() } - }, [historyData, wsHistoryEntries, isPublic, user, isLoading]) + }, [historyData, wsHistoryEntries, isPublic, user, isLoading, isSameUser]) const pendingRequests = useMemo(() => { if (!combinedEntries.length) return [] diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx index 63446914f..deaf38a98 100644 --- a/src/components/Payment/PaymentForm/index.tsx +++ b/src/components/Payment/PaymentForm/index.tsx @@ -69,7 +69,14 @@ export const PaymentForm = ({ const dispatch = useAppDispatch() const router = useRouter() const { user, fetchUser } = useAuth() - const { requestDetails, chargeDetails, daimoError, error: paymentStoreError, attachmentOptions } = usePaymentStore() + const { + requestDetails, + chargeDetails, + daimoError, + error: paymentStoreError, + attachmentOptions, + currentView, + } = usePaymentStore() const { setShowExternalWalletFulfillMethods, setExternalWalletFulfillMethod, @@ -178,6 +185,11 @@ export const PaymentForm = ({ }, [dispatch, recipient]) useEffect(() => { + // Skip balance check if on CONFIRM or STATUS view (balance has been optimistically updated) + if (currentView === 'CONFIRM' || currentView === 'STATUS') { + return + } + dispatch(paymentActions.setError(null)) const currentInputAmountStr = String(inputTokenAmount) @@ -261,6 +273,7 @@ export const PaymentForm = ({ selectedTokenData, isExternalWalletConnected, isExternalWalletFlow, + currentView, ]) // fetch token price From 9a11dbcb3713bfcc7bd4363022718e1ffa7d8071 Mon Sep 17 00:00:00 2001 From: Hugo Montenegro Date: Wed, 22 Oct 2025 20:10:51 -0300 Subject: [PATCH 2/2] fixes --- src/app/(mobile-ui)/history/page.tsx | 21 +++++++++++- src/app/(mobile-ui)/qr-pay/page.tsx | 51 ++++++++++++++++++++++++---- src/components/Claim/Claim.tsx | 4 +-- src/components/Home/HomeHistory.tsx | 22 +++++++++++- 4 files changed, 88 insertions(+), 10 deletions(-) diff --git a/src/app/(mobile-ui)/history/page.tsx b/src/app/(mobile-ui)/history/page.tsx index 3f7b54f7d..b2149941c 100644 --- a/src/app/(mobile-ui)/history/page.tsx +++ b/src/app/(mobile-ui)/history/page.tsx @@ -50,7 +50,26 @@ const HistoryPage = () => { // Process the entry through completeHistoryEntry to format amounts and add computed fields // This ensures WebSocket entries match the format of API-fetched entries - const completedEntry = await completeHistoryEntry(newEntry) + let completedEntry + try { + completedEntry = await completeHistoryEntry(newEntry) + } catch (error) { + console.error('[History] Failed to process WebSocket entry:', error) + Sentry.captureException(error, { + tags: { feature: 'websocket-history' }, + extra: { entryType: newEntry.type, entryUuid: newEntry.uuid }, + }) + + // Fallback: Use raw entry with minimal processing + completedEntry = { + ...newEntry, + timestamp: new Date(newEntry.timestamp), + extraData: { + ...newEntry.extraData, + usdAmount: newEntry.amount.toString(), // Best effort fallback + }, + } + } // Update TanStack Query cache with processed transaction queryClient.setQueryData>( diff --git a/src/app/(mobile-ui)/qr-pay/page.tsx b/src/app/(mobile-ui)/qr-pay/page.tsx index f07bac789..e2ebff575 100644 --- a/src/app/(mobile-ui)/qr-pay/page.tsx +++ b/src/app/(mobile-ui)/qr-pay/page.tsx @@ -151,24 +151,63 @@ export default function QRPayPage() { console.log('[SimpleFi WebSocket] Received status update:', entry.status) // Process entry through completeHistoryEntry to format amounts correctly - const completedEntry = await completeHistoryEntry(entry) + let completedEntry + try { + completedEntry = await completeHistoryEntry(entry) + } catch (error) { + console.error('[SimpleFi WebSocket] Failed to process entry:', error) + captureException(error, { + tags: { feature: 'simplefi-websocket' }, + extra: { entryUuid: entry.uuid }, + }) + setIsWaitingForWebSocket(false) + setPendingSimpleFiPaymentId(null) + setErrorMessage('We received an update, but failed to process it. Please check your history.') + setIsSuccess(false) + setLoadingState('Idle') + return + } setIsWaitingForWebSocket(false) setPendingSimpleFiPaymentId(null) switch (completedEntry.status) { - case 'approved': + case 'approved': { + // Guard against missing currency or simpleFiPayment data + if (!completedEntry.currency?.code || !completedEntry.currency?.amount) { + console.error('[SimpleFi WebSocket] Currency data missing on approval') + captureException(new Error('SimpleFi payment approved but currency details missing'), { + extra: { entryUuid: completedEntry.uuid }, + }) + setErrorMessage('Payment approved, but details are incomplete. Please check your history.') + setIsSuccess(false) + setLoadingState('Idle') + break + } + + if (!simpleFiPayment) { + console.error('[SimpleFi WebSocket] SimpleFi payment details missing on approval') + captureException(new Error('SimpleFi payment details missing on approval'), { + extra: { entryUuid: completedEntry.uuid }, + }) + setErrorMessage('Payment approved, but details are missing. Please check your history.') + setIsSuccess(false) + setLoadingState('Idle') + break + } + setSimpleFiPayment({ id: completedEntry.uuid, usdAmount: completedEntry.extraData?.usdAmount || completedEntry.amount, - currency: completedEntry.currency!.code, - currencyAmount: completedEntry.currency!.amount, - price: simpleFiPayment!.price, - address: simpleFiPayment!.address, + currency: completedEntry.currency.code, + currencyAmount: completedEntry.currency.amount, + price: simpleFiPayment.price, + address: simpleFiPayment.address, }) setIsSuccess(true) setLoadingState('Idle') break + } case 'expired': case 'canceled': diff --git a/src/components/Claim/Claim.tsx b/src/components/Claim/Claim.tsx index 89e1ca88c..51bfc4961 100644 --- a/src/components/Claim/Claim.tsx +++ b/src/components/Claim/Claim.tsx @@ -84,8 +84,8 @@ export const Claim = ({}) => { queryKey: ['sendLink', linkUrl], queryFn: () => sendLinksApi.get(linkUrl), enabled: !!linkUrl, // Only run when we have a link URL - retry: 3, // Retry 3 times for RPC sync issues - retryDelay: (attemptIndex) => (attemptIndex + 1) * 1000, // 1s, 2s, 3s (linear backoff) + retry: 4, // Retry a few times for DB replication lag + blockchain indexing + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), // Exponential: 1s, 2s, 4s, 8s (total ~15s) staleTime: 0, // Don't cache (one-time use per link) gcTime: 0, // Garbage collect immediately after use }) diff --git a/src/components/Home/HomeHistory.tsx b/src/components/Home/HomeHistory.tsx index f48668577..66f15d947 100644 --- a/src/components/Home/HomeHistory.tsx +++ b/src/components/Home/HomeHistory.tsx @@ -95,7 +95,27 @@ const HomeHistory = ({ isPublic = false, username }: { isPublic?: boolean; usern // Process WebSocket entries through completeHistoryEntry to format amounts correctly for (const wsEntry of sortedWsEntries) { - const completedEntry = await completeHistoryEntry(wsEntry) + let completedEntry + try { + completedEntry = await completeHistoryEntry(wsEntry) + } catch (error) { + console.error('[HomeHistory] Failed to process WebSocket entry:', error) + Sentry.captureException(error, { + tags: { feature: 'websocket-home-history' }, + extra: { entryType: wsEntry.type, entryUuid: wsEntry.uuid }, + }) + + // Fallback: Use raw entry with minimal processing + completedEntry = { + ...wsEntry, + timestamp: new Date(wsEntry.timestamp), + extraData: { + ...wsEntry.extraData, + usdAmount: wsEntry.amount.toString(), // Best effort fallback + }, + } + } + const existingIndex = entries.findIndex((entry) => entry.uuid === completedEntry.uuid) if (existingIndex !== -1) {