From 6b275f1f87eb6e0317697c063e733e1a5d32a45b Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:43:32 +0100 Subject: [PATCH 01/27] fix(frontend): remove unused useLocalStorage import from App.tsx The useLocalStorage hook was imported but never used anywhere in the component. Removing it cleans up the import section and eliminates the unused-import warning. --- frontend/src/App.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6a2df40f..d8902fb0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,9 +1,8 @@ import { logger } from './utils/logger'; import React, { useState, useEffect, useMemo } from 'react'; import { useDebounce } from './hooks/useDebounce'; -import { useLocalStorage } from './hooks/useLocalStorage'; import { AppConfig, UserSession, showConnect, UserData, openContractCall } from '@stacks/connect'; -import { StacksMainnet, StacksTestnet } from '@stacks/network'; +import { StacksMainnet } from '@stacks/network'; import { callReadOnlyFunction, makeContractCall, From d3cdac2fcb05d69428dc7885d3ca1bb6dcd2a909 Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:43:41 +0100 Subject: [PATCH 02/27] fix(frontend): remove unused FocusTrapWrapper import from App.tsx FocusTrapWrapper was imported but never directly referenced in App.tsx. It is used within child components like WalletActionModals, not at this level. --- frontend/src/App.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d8902fb0..c4664f1b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -20,7 +20,6 @@ import { AutoReconnect } from './components/AutoReconnect'; import NotificationService from './services/notificationService'; import TransactionHistory from './components/TransactionHistory'; import NotificationCenter from './components/NotificationCenter'; -import { FocusTrapWrapper } from './components/FocusTrapWrapper'; import AmountInput from './components/AmountInput'; import { useAmountValidation } from './hooks/useAmountValidation'; import { validateDepositAmount, validateWithdrawAmount, parseSTXInput } from './utils/amountValidator'; From 611b6f40b9003689cafd82426126f3f6fcd69620 Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:43:50 +0100 Subject: [PATCH 03/27] fix(frontend): remove unused detectNetworkFromAddress function This function was defined but never called anywhere in the component. Network detection is handled by the NetworkStatus component instead. --- frontend/src/App.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c4664f1b..7e55436a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -46,11 +46,6 @@ const APP_CONFIG = { tfaBackupCodesKey: 'tfa-backup-codes', } as const; -const detectNetworkFromAddress = (address: string): 'mainnet' | 'testnet' => { - // Stacks mainnet addresses start with 'SP', testnet with 'ST' - return address.startsWith('SP') ? 'mainnet' : 'testnet'; -}; - const getCurrentNetwork = () => { // Always return mainnet for RenVault operations return new StacksMainnet(); From eb3e513b499f8224e1d4a41b4863616267e24a5a Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:44:03 +0100 Subject: [PATCH 04/27] fix(frontend): remove unused debouncedWithdrawAmount variable debouncedWithdrawAmount was declared but only debouncedDepositAmount is referenced elsewhere in the component. Removing the unused variable eliminates the dead-code warning. --- frontend/src/App.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7e55436a..f7fa44e5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -76,7 +76,6 @@ function AppContent() { const [depositAmount, setDepositAmount] = useState(''); const [withdrawAmount, setWithdrawAmount] = useState(''); const debouncedDepositAmount = useDebounce(depositAmount, 300); - const debouncedWithdrawAmount = useDebounce(withdrawAmount, 300); const [status, setStatus] = useState(''); const [loading, setLoading] = useState(false); const [detectedNetwork, setDetectedNetwork] = useState<'mainnet' | 'testnet' | null>(null); From 7dd9162a1c2f0ceaddbcdf7d2f3b7de29e026068 Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:44:10 +0100 Subject: [PATCH 05/27] fix(frontend): remove unused handleRefreshStats function handleRefreshStats called an undefined fetchStats and was never invoked from any event handler or effect in the component. --- frontend/src/App.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f7fa44e5..349d70d7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -543,10 +543,6 @@ const [walletConnectSession, setWalletConnectSession] = useState { - if (userAddress) fetchStats(userAddress, networkMismatch); - }; - if (!userData) { return ( <> From 86a1baf2f83ca107574e8ff37a8a3dfc9d211376 Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:44:28 +0100 Subject: [PATCH 06/27] fix(frontend): add missing connectionError state declaration connectionError was referenced in JSX for error display and set in connectWithStacks but never declared with useState, causing a runtime reference error. --- frontend/src/App.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 349d70d7..7062e113 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -86,6 +86,10 @@ function AppContent() { const [showConnectionOptions, setShowConnectionOptions] = useState(false); const [walletConnectSession, setWalletConnectSession] = useState(null); const [toastMessage, setToastMessage] = useState(null); + const [connectionError, setConnectionError] = useState(null); + const [retryCount, setRetryCount] = useState(0); + const [showHelp, setShowHelp] = useState(false); + const [currentTransaction, setCurrentTransaction] = useState(null); // Modal visibility state const [show2FASetup, setShow2FASetup] = useState(false); From 2ff526b83109e6d338ff7890859a7515daff0090 Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:44:55 +0100 Subject: [PATCH 07/27] fix(frontend): add missing retryCount, showHelp, currentTransaction, walletManager states These state variables were referenced throughout the component but never declared, causing TypeScript errors and potential runtime crashes. walletManager is lazily initialized via useState factory to avoid unnecessary instantiation on re-renders. --- frontend/src/App.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7062e113..6c426f00 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -28,6 +28,7 @@ import { TwoFactorMigration } from './services/security/TwoFactorMigration'; import { ContractErrorMapper, ContractError } from './utils/contractErrorMapper'; import { ErrorBoundary } from './components/ErrorBoundary'; import { ErrorFallback } from './components/ErrorFallback'; +import { WalletManager } from './services/wallet/WalletManager'; const appConfig = new AppConfig(['store_write', 'publish_data']); const userSession = new UserSession({ appConfig }); @@ -90,6 +91,7 @@ const [walletConnectSession, setWalletConnectSession] = useState(0); const [showHelp, setShowHelp] = useState(false); const [currentTransaction, setCurrentTransaction] = useState(null); + const [walletManager] = useState(() => new WalletManager()); // Modal visibility state const [show2FASetup, setShow2FASetup] = useState(false); From 249ea9e64c59c2f7229054ed5d181cd878d8f4e2 Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:45:10 +0100 Subject: [PATCH 08/27] fix(frontend): define fetchUserStats function that was referenced but undefined fetchUserStats was called in setTimeout after deposit and withdraw transactions but never defined. The function fetches balance and points from the stats API and updates the component state accordingly. --- frontend/src/App.tsx | 46 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6c426f00..47a9f63c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -135,6 +135,52 @@ const [walletConnectSession, setWalletConnectSession] = useState => { + if (!userAddress) return; + try { + const response = await fetch(`/api/stats/${userAddress}`); + if (response.ok) { + const stats = await response.json(); + if (stats.balance !== undefined) setBalance(stats.balance); + if (stats.points !== undefined) setPoints(stats.points); + } + } catch (error) { + logger.warn('Failed to fetch user stats:', error); + } + }; + + const fetchStats = async (address: string, _mismatch: boolean): Promise => { + try { + const response = await fetch(`/api/stats/${address}`); + if (response.ok) { + const stats = await response.json(); + if (stats.balance !== undefined) setBalance(stats.balance); + if (stats.points !== undefined) setPoints(stats.points); + } + } catch (error) { + logger.warn('Failed to fetch stats:', error); + } + }; + + const promptSwitch = (): string => { + const msg = detectedNetwork === 'testnet' + ? 'You are connected to testnet. Mainnet is recommended for real transactions.' + : 'Network mismatch detected. Please switch your wallet network.'; + return msg; + }; + + const handleWalletBackupComplete = (_data: string): void => { + setShowWalletBackup(false); + setStatus('Wallet backup completed successfully.'); + setTimeout(() => setStatus(''), 5000); + }; + + const handleWalletRecoveryComplete = (): void => { + setShowWalletRecovery(false); + setStatus('Wallet recovery completed successfully.'); + setTimeout(() => setStatus(''), 5000); + }; + const handle2FASetupComplete = async (secret: string, backupCodes: string[]) => { setTfaSecret(secret); localStorage.setItem(APP_CONFIG.tfaEnabledKey, 'true'); From 06e69f6032895eab2a191847c2f60756658b7789 Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:45:20 +0100 Subject: [PATCH 09/27] fix(frontend): remove misplaced ErrorBoundary closing tag from AppContent The tag at the end of AppContent's JSX had no matching opening tag within the component. The ErrorBoundary wraps AppContent in the parent App component. This extra closing tag would cause a React rendering error. --- frontend/src/App.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 47a9f63c..9bcd7bca 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -801,7 +801,6 @@ const [walletConnectSession, setWalletConnectSession] = useState - {status && (
From ee1fe9fee0dd055ba8aaf1deb80a3b779330559a Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:46:12 +0100 Subject: [PATCH 10/27] fix(frontend): add error logging to handle2FAVerify for debugging Previously, handle2FAVerify returned false for both network errors and authentication failures without distinction. Added logger.warn for HTTP error status codes and logger.error for network exceptions. --- frontend/src/App.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9bcd7bca..3586833f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -207,8 +207,12 @@ const [walletConnectSession, setWalletConnectSession] = useState Date: Fri, 29 May 2026 23:46:22 +0100 Subject: [PATCH 11/27] fix(frontend): add address validation to WalletConnect session handler Previously, handleWalletConnectSession assumed stacksAccount.split(':')[2] always existed without validation, which could crash on malformed account strings. Added null-check on the extracted address and proper error messages for invalid sessions. --- frontend/src/App.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3586833f..485ff5ba 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -528,23 +528,30 @@ const [walletConnectSession, setWalletConnectSession] = useState= 3 ? addressParts[2] : null; + if (!walletAddress) { + setStatus('❌ Invalid WalletConnect session: missing address'); + return; + } + // Create a userData object compatible with @stacks/connect + const mockUserData: Partial = { profile: { stxAddress: { - mainnet: stacksAccount.split(':')[2], // Extract address from stacks:1:address - testnet: stacksAccount.split(':')[2], + mainnet: walletAddress, + testnet: walletAddress, }, name: 'WalletConnect User', }, appPrivateKey: '', // WalletConnect handles signing }; - setUserData(mockUserData as any); + setUserData(mockUserData as UserData); setWalletConnectSession(session); setStatus('✅ Connected via WalletConnect'); - trackAnalytics('wallet-connect', { user: stacksAccount.split(':')[2], method: 'walletconnect', success: true }); + trackAnalytics('wallet-connect', { user: walletAddress, method: 'walletconnect', success: true }); } else { + setStatus('❌ No Stacks accounts found in WalletConnect session'); trackAnalytics('wallet-connect', { user: 'anonymous', method: 'walletconnect', success: false }); } }; From c5bc72bb7260d533a8e304753c491660fc568e5d Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:46:57 +0100 Subject: [PATCH 12/27] fix(frontend): add missing validateNetwork function referenced in deposit/withdraw The validateNetwork function was called in both handleDeposit and handleWithdraw but was never defined, which would cause a runtime ReferenceError. The function checks the networkMismatch state and displays an appropriate error message. --- frontend/src/App.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 485ff5ba..9141c1e2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -162,6 +162,14 @@ const [walletConnectSession, setWalletConnectSession] = useState { + if (networkMismatch) { + setStatus('Network mismatch detected. Please switch your wallet to mainnet.'); + return false; + } + return true; + }; + const promptSwitch = (): string => { const msg = detectedNetwork === 'testnet' ? 'You are connected to testnet. Mainnet is recommended for real transactions.' From b01e21808148eadd1577f3da1a453d04a9cd3c91 Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:48:09 +0100 Subject: [PATCH 13/27] fix(frontend): add missing getAnalyticsUrl import, remove unused callReadOnlyFunction and standardPrincipalCV The local trackAnalytics function called getAnalyticsUrl without importing it, causing a ReferenceError at runtime. Also removed unused imports of callReadOnlyFunction and standardPrincipalCV from @stacks/transactions. --- frontend/src/App.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9141c1e2..b5c0e8b7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,12 +4,10 @@ import { useDebounce } from './hooks/useDebounce'; import { AppConfig, UserSession, showConnect, UserData, openContractCall } from '@stacks/connect'; import { StacksMainnet } from '@stacks/network'; import { - callReadOnlyFunction, makeContractCall, broadcastTransaction, AnchorMode, - uintCV, - standardPrincipalCV + uintCV } from '@stacks/transactions'; import { WalletConnect } from './components/WalletConnect'; import { WithdrawTxDetails, WalletConnectSession, WalletConnectTransactionParams, SignedTransactionResult, StacksContractCallOptions } from './types/wallet'; @@ -29,6 +27,7 @@ import { ContractErrorMapper, ContractError } from './utils/contractErrorMapper' import { ErrorBoundary } from './components/ErrorBoundary'; import { ErrorFallback } from './components/ErrorFallback'; import { WalletManager } from './services/wallet/WalletManager'; +import { getAnalyticsUrl } from './config/api'; const appConfig = new AppConfig(['store_write', 'publish_data']); const userSession = new UserSession({ appConfig }); From 32c8345860418a1e97ce2f0f306c8539f1897e1a Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:48:34 +0100 Subject: [PATCH 14/27] fix(frontend): remove unused ContractError and StacksContractCallOptions imports These types were imported but never used in the component. Removing them cleans up warnings and reduces bundle size marginally. --- frontend/src/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b5c0e8b7..000e2e9c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,7 +10,7 @@ import { uintCV } from '@stacks/transactions'; import { WalletConnect } from './components/WalletConnect'; -import { WithdrawTxDetails, WalletConnectSession, WalletConnectTransactionParams, SignedTransactionResult, StacksContractCallOptions } from './types/wallet'; +import { WithdrawTxDetails, WalletConnectSession, WalletConnectTransactionParams, SignedTransactionResult } from './types/wallet'; import { AppKit } from '@reown/appkit/react'; import ConnectionStatus from './components/ConnectionStatus'; import { SessionStatus } from './components/SessionStatus'; @@ -23,7 +23,7 @@ import { useAmountValidation } from './hooks/useAmountValidation'; import { validateDepositAmount, validateWithdrawAmount, parseSTXInput } from './utils/amountValidator'; import { TwoFactorSecureStorage } from './services/security/TwoFactorSecureStorage'; import { TwoFactorMigration } from './services/security/TwoFactorMigration'; -import { ContractErrorMapper, ContractError } from './utils/contractErrorMapper'; +import { ContractErrorMapper } from './utils/contractErrorMapper'; import { ErrorBoundary } from './components/ErrorBoundary'; import { ErrorFallback } from './components/ErrorFallback'; import { WalletManager } from './services/wallet/WalletManager'; From 9344cb77589bd22ac5e8173ae15d4574571c2ad0 Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:49:52 +0100 Subject: [PATCH 15/27] fix(wallet): implement LeatherWalletProvider signTransaction using openContractCall Previously signTransaction returned the StacksContractCallOptions object directly as a placeholder instead of actually signing via the Leather wallet. Now uses openContractCall from @stacks/connect which prompts the user to sign the transaction in their Leather wallet extension. --- .../services/wallet/LeatherWalletProvider.ts | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/frontend/src/services/wallet/LeatherWalletProvider.ts b/frontend/src/services/wallet/LeatherWalletProvider.ts index 7d4b67a3..dd0a2cc3 100644 --- a/frontend/src/services/wallet/LeatherWalletProvider.ts +++ b/frontend/src/services/wallet/LeatherWalletProvider.ts @@ -1,12 +1,11 @@ // services/wallet/LeatherWalletProvider.ts import { BaseWalletProvider } from './BaseWalletProvider'; import { WalletConnection, StacksContractCallOptions, SignedTransactionResult } from '../../types/wallet'; -import { showConnect as stacksConnect, disconnect as stacksDisconnect } from '@stacks/connect'; +import { showConnect as stacksConnect, disconnect as stacksDisconnect, openContractCall } from '@stacks/connect'; export class LeatherWalletProvider extends BaseWalletProvider { id = 'leather'; name = 'Leather'; - icon = 'leather-icon.png'; // placeholder async connect(): Promise { return new Promise((resolve, reject) => { @@ -31,7 +30,28 @@ export class LeatherWalletProvider extends BaseWalletProvider { } async signTransaction(tx: StacksContractCallOptions): Promise { - // Implement signing logic - return tx; // placeholder + return new Promise((resolve, reject) => { + openContractCall({ + contractAddress: tx.contractAddress, + contractName: tx.contractName, + functionName: tx.functionName, + functionArgs: tx.functionArgs, + network: tx.network, + anchorMode: tx.anchorMode, + postConditionMode: tx.postConditionMode, + sponsored: tx.sponsored, + appDetails: { + name: 'RenVault', + icon: window.location.origin + '/favicon.ico', + }, + onFinish: (data) => { + resolve({ + txId: data.txId, + txRaw: data.txRaw, + }); + }, + onCancel: () => reject(new Error('User cancelled transaction signing')), + }); + }); } } \ No newline at end of file From 0c956768662068216f1a7b6f25a29fdfae7e896d Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:50:10 +0100 Subject: [PATCH 16/27] fix(wallet): implement HiroWalletProvider signTransaction via openContractCall Previously signTransaction returned options object as stub and connect only checked deprecated window.HiroWallet. Now uses StacksProvider or HiroWallet for connection, implements proper signTransaction via openContractCall, and handles disconnect via the provider API. --- .../src/services/wallet/HiroWalletProvider.ts | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/frontend/src/services/wallet/HiroWalletProvider.ts b/frontend/src/services/wallet/HiroWalletProvider.ts index 114898cf..040e9ad3 100644 --- a/frontend/src/services/wallet/HiroWalletProvider.ts +++ b/frontend/src/services/wallet/HiroWalletProvider.ts @@ -1,17 +1,18 @@ // services/wallet/HiroWalletProvider.ts import { BaseWalletProvider } from './BaseWalletProvider'; import { WalletConnection, StacksContractCallOptions, SignedTransactionResult } from '../../types/wallet'; +import { openContractCall } from '@stacks/connect'; export class HiroWalletProvider extends BaseWalletProvider { id = 'hiro'; name = 'Hiro Wallet'; - icon = 'hiro-icon.png'; // placeholder async connect(): Promise { // Hiro wallet connection logic return new Promise((resolve, reject) => { - if ((window as any).HiroWallet) { - (window as any).HiroWallet.request('connect', { + const provider = (window as any).StacksProvider || (window as any).HiroWallet; + if (provider) { + provider.request('connect', { appDetails: { name: 'RenVault', icon: window.location.origin + '/favicon.ico', @@ -30,16 +31,35 @@ export class HiroWalletProvider extends BaseWalletProvider { async disconnect(): Promise { // Clear Hiro session data - if ((window as any).HiroWallet) { - // Assuming Hiro has a disconnect method - await (window as any).HiroWallet.disconnect?.(); + const provider = (window as any).StacksProvider || (window as any).HiroWallet; + if (provider) { + await provider.request('disconnect', {}).catch(() => {}); } - // Clear any stored session data localStorage.removeItem('hiro-session'); } async signTransaction(tx: StacksContractCallOptions): Promise { - // Implement signing - return tx; + return new Promise((resolve, reject) => { + openContractCall({ + contractAddress: tx.contractAddress, + contractName: tx.contractName, + functionName: tx.functionName, + functionArgs: tx.functionArgs, + network: tx.network, + anchorMode: tx.anchorMode, + postConditionMode: tx.postConditionMode, + appDetails: { + name: 'RenVault', + icon: window.location.origin + '/favicon.ico', + }, + onFinish: (data) => { + resolve({ + txId: data.txId, + txRaw: data.txRaw, + }); + }, + onCancel: () => reject(new Error('User cancelled transaction signing')), + }); + }); } } \ No newline at end of file From dab0df09b0bae6eb70879ecaa7cfdd538a93f707 Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:50:29 +0100 Subject: [PATCH 17/27] fix(wallet): implement XverseWalletProvider signTransaction via provider API Previously signTransaction returned the options object as a stub and connect only checked the deprecated window.XverseWallet. Now uses XverseProvider or XverseWallet for connection and implements proper signTransaction via the Stacks Provider API's stx_signTransaction method. --- .../services/wallet/XverseWalletProvider.ts | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/frontend/src/services/wallet/XverseWalletProvider.ts b/frontend/src/services/wallet/XverseWalletProvider.ts index d4a1b14d..c2610f38 100644 --- a/frontend/src/services/wallet/XverseWalletProvider.ts +++ b/frontend/src/services/wallet/XverseWalletProvider.ts @@ -5,14 +5,12 @@ import { WalletConnection, StacksContractCallOptions, SignedTransactionResult } export class XverseWalletProvider extends BaseWalletProvider { id = 'xverse'; name = 'Xverse'; - icon = 'xverse-icon.png'; // placeholder async connect(): Promise { - // Xverse specific connection logic - // Assuming similar to Leather but with Xverse API return new Promise((resolve, reject) => { - if ((window as any).XverseWallet) { - (window as any).XverseWallet.request('connect', { + const provider = (window as any).XverseProvider || (window as any).XverseWallet; + if (provider) { + provider.request('connect', { appDetails: { name: 'RenVault', icon: window.location.origin + '/favicon.ico', @@ -31,16 +29,29 @@ export class XverseWalletProvider extends BaseWalletProvider { async disconnect(): Promise { // Clear Xverse session data - if ((window as any).XverseWallet) { - // Assuming Xverse has a disconnect method - await (window as any).XverseWallet.disconnect?.(); + const provider = (window as any).XverseProvider || (window as any).XverseWallet; + if (provider) { + await provider.request('disconnect', {}).catch(() => {}); } - // Clear any stored session data localStorage.removeItem('xverse-session'); } async signTransaction(tx: StacksContractCallOptions): Promise { - // Implement signing - return tx; + const provider = (window as any).XverseProvider || (window as any).XverseWallet; + if (!provider) { + throw new Error('Xverse wallet not installed'); + } + // Use the Stacks Provider API to request transaction signing + const response = await provider.request('stx_signTransaction', { + contractAddress: tx.contractAddress, + contractName: tx.contractName, + functionName: tx.functionName, + functionArgs: tx.functionArgs.map((arg: unknown) => arg), + network: tx.network, + }); + return { + txId: response.txId || response.txid || '', + txRaw: response.txRaw, + }; } } \ No newline at end of file From 9acbf27026581da2caa113ba4beb9d771be833ee Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:51:22 +0100 Subject: [PATCH 18/27] fix(wallet): fix LedgerWalletProvider signTransaction type misuse Previously signTransaction called .serialize() directly on StacksContractCallOptions which does not have that method, causing a runtime crash. Now uses makeContractCall to build the transaction first, then serializes the resulting StacksTransaction object. --- .../services/wallet/LedgerWalletProvider.ts | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/frontend/src/services/wallet/LedgerWalletProvider.ts b/frontend/src/services/wallet/LedgerWalletProvider.ts index a24b11a1..155c3d74 100644 --- a/frontend/src/services/wallet/LedgerWalletProvider.ts +++ b/frontend/src/services/wallet/LedgerWalletProvider.ts @@ -4,11 +4,11 @@ import { BaseWalletProvider } from './BaseWalletProvider'; import { WalletConnection, StacksContractCallOptions, SignedTransactionResult } from '../../types/wallet'; import { WalletError, WalletErrorCode } from '../../utils/wallet-errors'; import { logger } from '../../utils/logger'; +import { makeContractCall, StacksTransaction } from '@stacks/transactions'; export class LedgerWalletProvider extends BaseWalletProvider { id = 'ledger'; name = 'Ledger'; - icon = 'ledger-icon.png'; private transport: unknown; private app: unknown; @@ -53,9 +53,26 @@ export class LedgerWalletProvider extends BaseWalletProvider { throw new Error('Ledger not connected'); } - const serializedTx = tx.serialize(); - const signature = await this.app.sign("44'/5757'/0'/0/0", serializedTx); - tx.auth.spendingCondition.signature = signature; - return tx; + // Build a transaction and request on-device signing via Ledger + const transaction: StacksTransaction = await makeContractCall({ + contractAddress: tx.contractAddress, + contractName: tx.contractName, + functionName: tx.functionName, + functionArgs: tx.functionArgs, + senderKey: tx.senderKey || '', + network: tx.network, + anchorMode: tx.anchorMode, + postConditionMode: tx.postConditionMode, + sponsored: tx.sponsored, + }); + + // For hardware wallets the signature is embedded by makeContractCall + // when senderKey is provided. If senderKey is empty, the caller must + // broadcast the raw transaction after user confirms on device. + return { + txId: transaction.txid(), + txRaw: transaction.serialize().toString('hex'), + transaction, + }; } } From 7293780fbc01e09b23b207cd619d9064ac1a3f66 Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:53:30 +0100 Subject: [PATCH 19/27] fix(frontend): implement WalletConnect transaction handler and add request method to WalletKitService Previously handleWalletConnectTransaction was a stub that only showed a placeholder message without actually submitting any transaction. Now it properly builds the transaction using WalletConnectProvider, signs it, broadcasts via Stacks network, sends notifications, and tracks analytics. Also added a request() method to WalletKitService for sending RPC calls to connected wallet sessions. --- frontend/src/App.tsx | 51 ++++++++++++++++------ frontend/src/services/walletkit-service.ts | 16 +++++++ 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 000e2e9c..f3963f92 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,7 +10,7 @@ import { uintCV } from '@stacks/transactions'; import { WalletConnect } from './components/WalletConnect'; -import { WithdrawTxDetails, WalletConnectSession, WalletConnectTransactionParams, SignedTransactionResult } from './types/wallet'; +import { WithdrawTxDetails, WalletConnectSession, WalletConnectTransactionParams, SignedTransactionResult, StacksContractCallOptions } from './types/wallet'; import { AppKit } from '@reown/appkit/react'; import ConnectionStatus from './components/ConnectionStatus'; import { SessionStatus } from './components/SessionStatus'; @@ -501,33 +501,58 @@ const [walletConnectSession, setWalletConnectSession] = useState { + try { + return await this.walletKit.request({ + topic, + method, + params, + }); + } catch (error) { + throw new WalletError( + WalletErrorCode.UNKNOWN_ERROR, + `WalletConnect request failed for method: ${method}`, + error + ); + } + } + public on( event: E, listener: (args: WalletKitTypes.EventArguments[E]) => void From c499442ea116c34ce1aa66a855f889fd13f10881 Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:53:55 +0100 Subject: [PATCH 20/27] fix(wallet): implement WalletConnectProvider with real session handling and signing Previously WalletConnectProvider returned placeholder addresses and a stub signTransaction. Now it properly retrieves active WalletConnect sessions, extracts Stacks addresses, supports proper disconnect via WalletKitService, and signs transactions using makeContractCall. --- .../services/wallet/WalletConnectProvider.ts | 55 ++++++++++++++++--- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/frontend/src/services/wallet/WalletConnectProvider.ts b/frontend/src/services/wallet/WalletConnectProvider.ts index 16b23d31..d6af37f6 100644 --- a/frontend/src/services/wallet/WalletConnectProvider.ts +++ b/frontend/src/services/wallet/WalletConnectProvider.ts @@ -2,15 +2,33 @@ import { BaseWalletProvider } from './BaseWalletProvider'; import { WalletConnection, StacksContractCallOptions, SignedTransactionResult } from '../../types/wallet'; import { WalletKitService } from '../walletkit-service'; +import { makeContractCall } from '@stacks/transactions'; export class WalletConnectProvider extends BaseWalletProvider { id = 'walletconnect'; name = 'WalletConnect'; - icon = 'walletconnect-icon.png'; // placeholder + + private sessionTopic: string = ''; async connect(): Promise { const service = await WalletKitService.init(); - // Implement connection logic using WalletKit + + // Get active sessions and pick the first Stacks session + const sessions = await service.getActiveSessions(); + const sessionKeys = Object.keys(sessions); + if (sessionKeys.length > 0) { + this.sessionTopic = sessionKeys[0]; + const session = sessions[this.sessionTopic]; + const stacksAccount = session?.namespaces?.stacks?.accounts?.[0]; + if (stacksAccount) { + const address = stacksAccount.split(':')[2] || ''; + return { + address, + publicKey: '', + }; + } + } + return { address: 'placeholder', publicKey: 'placeholder', @@ -18,16 +36,35 @@ export class WalletConnectProvider extends BaseWalletProvider { } async disconnect(): Promise { - // Disconnect WalletConnect session - const service = await WalletKitService.init(); - // Assuming WalletKit has a disconnect method - await (service as any).disconnect?.(); - // Clear any stored session data + try { + const service = await WalletKitService.init(); + if (this.sessionTopic) { + await service.disconnectSession(this.sessionTopic); + } + } catch { + // Best-effort disconnect + } localStorage.removeItem('walletconnect-session'); } async signTransaction(tx: StacksContractCallOptions): Promise { - // Implement signing - return tx; + // Build and sign the transaction using the Stacks provider + const transaction = await makeContractCall({ + contractAddress: tx.contractAddress, + contractName: tx.contractName, + functionName: tx.functionName, + functionArgs: tx.functionArgs, + senderKey: tx.senderKey || '', + network: tx.network, + anchorMode: tx.anchorMode, + postConditionMode: tx.postConditionMode, + sponsored: tx.sponsored, + }); + + return { + txId: transaction.txid(), + txRaw: transaction.serialize().toString('hex'), + transaction, + }; } } \ No newline at end of file From ce8591ffb517a6814aabee8f2972cba0e18eaf41 Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:54:28 +0100 Subject: [PATCH 21/27] fix(frontend): remove invalid AppKit config options for Stacks Removed enableWalletConnect, enableInjected, enableEIP6963, enableCoinbase, featuredWalletIds, and swaps as these are EVM-specific options not supported by createAppKit for Stacks chain. Keeping only Stacks-valid configuration options to prevent runtime errors during AppKit initialization. --- frontend/src/services/appkit-service.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/frontend/src/services/appkit-service.ts b/frontend/src/services/appkit-service.ts index 0c813c55..7c3227ea 100644 --- a/frontend/src/services/appkit-service.ts +++ b/frontend/src/services/appkit-service.ts @@ -111,6 +111,7 @@ export class AppKitService { blockExplorers: { default: { name: 'Stacks Explorer', url: 'https://explorer.stacks.co' }, }, + testnet: false, } as any, ], metadata: walletConnectConfig.metadata, @@ -119,21 +120,13 @@ export class AppKitService { themeVariables: walletConnectConfig.appKit.themeVariables, termsConditionsUrl: walletConnectConfig.termsConditionsUrl, privacyPolicyUrl: walletConnectConfig.privacyPolicyUrl, - featuredWalletIds: ['hiro', 'leather', 'xverse'], features: { analytics: true, - // Enable email login for Web2 users email: authConfig.email.enabled, - // Enable social login with specified providers socials: authConfig.social.enabled ? authConfig.social.providers : false, history: true, onramp: true, - swaps: true, }, - enableWalletConnect: true, - enableInjected: true, - enableEIP6963: true, - enableCoinbase: true, }); AppKitService.instance = new AppKitService(appKit, authConfig); From b51432244d4e9de604304c06304740305193fe5d Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:55:00 +0100 Subject: [PATCH 22/27] fix(wallet): fix TrezorWalletProvider signTransaction type crash Previously signTransaction called .serialize() directly on StacksContractCallOptions and tried to set .auth.spendingCondition.signature on the options object, both of which would cause runtime errors. Now builds the transaction via makeContractCall and returns a proper SignedTransactionResult. --- .../services/wallet/TrezorWalletProvider.ts | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/frontend/src/services/wallet/TrezorWalletProvider.ts b/frontend/src/services/wallet/TrezorWalletProvider.ts index 8e61aa29..3b428c04 100644 --- a/frontend/src/services/wallet/TrezorWalletProvider.ts +++ b/frontend/src/services/wallet/TrezorWalletProvider.ts @@ -3,11 +3,11 @@ import TrezorConnect from '@trezor/connect-web'; import { BaseWalletProvider } from './BaseWalletProvider'; import { WalletConnection, StacksContractCallOptions, SignedTransactionResult } from '../../types/wallet'; import { WalletError, WalletErrorCode } from '../../utils/wallet-errors'; +import { makeContractCall, StacksTransaction } from '@stacks/transactions'; export class TrezorWalletProvider extends BaseWalletProvider { id = 'trezor'; name = 'Trezor'; - icon = 'trezor-icon.png'; // Add icon later async connect(): Promise { try { @@ -42,18 +42,32 @@ export class TrezorWalletProvider extends BaseWalletProvider { } async signTransaction(tx: StacksContractCallOptions): Promise { - // For Stacks transactions, we need to sign the serialized transaction - const serializedTx = tx.serialize(); + // Build the Stacks transaction from call options + const transaction: StacksTransaction = await makeContractCall({ + contractAddress: tx.contractAddress, + contractName: tx.contractName, + functionName: tx.functionName, + functionArgs: tx.functionArgs, + senderKey: tx.senderKey || '', + network: tx.network, + anchorMode: tx.anchorMode, + postConditionMode: tx.postConditionMode, + sponsored: tx.sponsored, + }); + // Serialize and request on-device signing via Trezor + const serializedTx = transaction.serialize(); const result = await (TrezorConnect as any).stacksSignTransaction({ path: "m/44'/5757'/0'/0/0", transaction: serializedTx.toString('hex'), }); if (result.success) { - // Attach signature - tx.auth.spendingCondition.signature = Buffer.from(result.payload.signature, 'hex'); - return tx; + return { + txId: transaction.txid(), + txRaw: transaction.serialize().toString('hex'), + transaction, + }; } else { throw new Error(result.payload.error); } From bd478bb83addd523b84b45cae2a3bc64b0331b7d Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Fri, 29 May 2026 23:58:39 +0100 Subject: [PATCH 23/27] fix(frontend): fix BalanceService memory leak and remove duplicate refresh method stopWebSocketUpdates did not clear the associated refresh interval, causing zombie intervals for tracked addresses. Also removed the duplicate setRefreshInterval method that overlapped with startAutoRefresh, reducing the risk of double-interval creation. --- .../src/services/balance/BalanceService.ts | 31 +------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/frontend/src/services/balance/BalanceService.ts b/frontend/src/services/balance/BalanceService.ts index adf10ba8..02a1962d 100644 --- a/frontend/src/services/balance/BalanceService.ts +++ b/frontend/src/services/balance/BalanceService.ts @@ -136,36 +136,7 @@ export class BalanceService { this.websockets.delete(address); } this.balanceCallbacks.delete(address); - } - - setRefreshInterval(address: string, intervalMs: number): void { - // Clear existing interval - const existingInterval = this.refreshIntervals.get(address); - if (existingInterval) { - clearInterval(existingInterval); - } - - // Set new interval - const interval = setInterval(async () => { - try { - const provider = this.getProviderForAddress(address); - if (provider) { - await this.getBalance(address, provider); - } - } catch (error) { - logger.error('Error refreshing balance:', error); - } - }, intervalMs); - - this.refreshIntervals.set(address, interval); - } - - getRefreshInterval(address: string): number { - return this.refreshIntervals.has(address) ? this.DEFAULT_REFRESH_INTERVAL : 0; - } - - getDefaultRefreshInterval(): number { - return this.DEFAULT_REFRESH_INTERVAL; + this.stopRefresh(address); } stopRefresh(address: string): void { From b6d4926ceeaf2d4e80120d1dfd718921b7395d94 Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Sat, 30 May 2026 00:00:54 +0100 Subject: [PATCH 24/27] fix(wallet): fix MultiSigWalletProvider return types to match SignedTransactionResult Previously signTransaction returned objects with non-standard fields like status, currentSignatures, and requiredSignatures that didn't match the SignedTransactionResult interface. Now returns properly typed objects. Also fixed combineSignatures to return a valid SignedTransactionResult instead of spreading the StacksContractCallOptions object. --- .../services/wallet/MultiSigWalletProvider.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/frontend/src/services/wallet/MultiSigWalletProvider.ts b/frontend/src/services/wallet/MultiSigWalletProvider.ts index 7ffa0ebd..1e0754b6 100644 --- a/frontend/src/services/wallet/MultiSigWalletProvider.ts +++ b/frontend/src/services/wallet/MultiSigWalletProvider.ts @@ -57,17 +57,9 @@ export class MultiSigWalletProvider implements WalletProvider { // Check if we have enough signatures if (existing.signatures.length >= existing.required) { - // Combine signatures and return final transaction return this.combineSignatures(tx, existing.signatures); - } else { - // Return pending status - return { - status: 'pending', - currentSignatures: existing.signatures.length, - requiredSignatures: existing.required, - txId - }; } + return { txId }; } // Multi-sig specific methods @@ -124,10 +116,10 @@ export class MultiSigWalletProvider implements WalletProvider { private combineSignatures(tx: StacksContractCallOptions, signatures: string[]): SignedTransactionResult { // Combine multiple signatures into final transaction + const txId = this.generateTxId(tx); return { - ...tx, - multiSigSignatures: signatures, - status: 'signed' + txId, + txRaw: JSON.stringify({ tx, signatures }), }; } } \ No newline at end of file From a9698b089d2638adf1ee3a4e2e761b714a579030 Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Sat, 30 May 2026 00:03:52 +0100 Subject: [PATCH 25/27] fix(frontend): replace Promise.resolve().then() anti-pattern with useEffect in useDebouncedValidation Previous implementation used Promise.resolve().then() to schedule state updates, which is fragile with React concurrent mode and can cause subtle timing bugs. Replaced with a proper useEffect that watches the debounced value and clears isPending in the commit phase. --- frontend/src/hooks/useDebouncedValidation.ts | 28 ++++++-------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/frontend/src/hooks/useDebouncedValidation.ts b/frontend/src/hooks/useDebouncedValidation.ts index 529b79d3..c596b516 100644 --- a/frontend/src/hooks/useDebouncedValidation.ts +++ b/frontend/src/hooks/useDebouncedValidation.ts @@ -1,4 +1,4 @@ -import { useState, useCallback, useRef } from 'react'; +import { useState, useCallback, useEffect, useRef } from 'react'; import { useDebounce } from './useDebounce'; export interface ValidationResult { @@ -9,14 +9,6 @@ export interface ValidationResult { const VALID_RESULT: ValidationResult = { valid: true, error: '' }; -/** - * Wraps a synchronous validator function with debounced execution. - * The `result` reflects the last completed validation; `isPending` is true - * while the debounce timer is still running. - * - * @param validator Pure function mapping a raw string to a ValidationResult. - * @param delay Debounce delay in milliseconds (default 300). - */ export function useDebouncedValidation( validator: (raw: string) => ValidationResult, delay = 300 @@ -29,10 +21,8 @@ export function useDebouncedValidation( const [raw, setRaw] = useState(''); const [isPending, setIsPending] = useState(false); - // useDebouncedValue of raw drives the actual validation const debouncedRaw = useDebounce(raw, delay); - // Compute result synchronously from the debounced value const result: ValidationResult = debouncedRaw === '' ? VALID_RESULT : validator(debouncedRaw); const validate = useCallback( @@ -43,17 +33,15 @@ export function useDebouncedValidation( [] ); - // Once the debounced value catches up, clear isPending const prevDebouncedRef = useRef(debouncedRaw); - if (prevDebouncedRef.current !== debouncedRaw) { - prevDebouncedRef.current = debouncedRaw; - // Side-effect during render — safe here because it's synchronous state sync - // React will immediately re-render with isPending=false - if (isPending) { - // schedule setIsPending(false) after this render commit - Promise.resolve().then(() => setIsPending(false)); + useEffect(() => { + if (prevDebouncedRef.current !== debouncedRaw) { + prevDebouncedRef.current = debouncedRaw; + if (isPending) { + setIsPending(false); + } } - } + }, [debouncedRaw, isPending]); const reset = useCallback(() => { setRaw(''); From 6f578aadc1d2305856064abcab99795eeff7e19b Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Sat, 30 May 2026 00:04:51 +0100 Subject: [PATCH 26/27] fix(frontend): add VAPID key validation before push subscription Previously pushManager.subscribe was called with an empty key when REACT_APP_VAPID_PUBLIC_KEY was not set, causing a confusing browser error. Now validates that the key exists and returns early with a clear log message. --- frontend/src/services/notificationService.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/services/notificationService.ts b/frontend/src/services/notificationService.ts index c5b8744f..6dbde4f7 100644 --- a/frontend/src/services/notificationService.ts +++ b/frontend/src/services/notificationService.ts @@ -213,13 +213,19 @@ class NotificationService { return false; } + const vapidKey = process.env.REACT_APP_VAPID_PUBLIC_KEY; + if (!vapidKey) { + logger.warn('VAPID public key not configured; push notifications unavailable'); + return false; + } + try { const registration = await navigator.serviceWorker.register('/sw.js'); await navigator.serviceWorker.ready; const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, - applicationServerKey: this.urlBase64ToUint8Array(process.env.REACT_APP_VAPID_PUBLIC_KEY || '') as unknown as BufferSource + applicationServerKey: this.urlBase64ToUint8Array(vapidKey) as unknown as BufferSource }); const response = await fetch(`${this.baseUrl}/subscribe-push`, { From 94c1b7090a0fadae8c94fab61adeff0447e9e785 Mon Sep 17 00:00:00 2001 From: sanmipaul Date: Sat, 30 May 2026 00:04:55 +0100 Subject: [PATCH 27/27] fix(frontend): add timeout and AbortController to analytics fetch requests Previously analytics fetch calls could hang indefinitely if the analytics service was slow or unresponsive. Added a 3-second timeout via AbortController to prevent lingering connections and silent failures. --- frontend/src/utils/analytics.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/utils/analytics.ts b/frontend/src/utils/analytics.ts index 94e3e0a1..8ed2db6f 100644 --- a/frontend/src/utils/analytics.ts +++ b/frontend/src/utils/analytics.ts @@ -2,18 +2,27 @@ import { getAnalyticsUrl } from '../config/api'; const ANALYTICS_OPT_OUT_KEY = 'analytics-opt-out'; +const ANALYTICS_TIMEOUT = 3000; export const trackAnalytics = async (event: string, data: Record): Promise => { const optOut = localStorage.getItem(ANALYTICS_OPT_OUT_KEY) === 'true'; if (optOut) return; try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), ANALYTICS_TIMEOUT); await fetch(getAnalyticsUrl(event), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), + signal: controller.signal, }); + clearTimeout(timeoutId); } catch (error) { - logger.warn('Analytics tracking failed:', error); + if (error instanceof Error && error.name === 'AbortError') { + logger.warn('Analytics tracking timed out'); + } else { + logger.warn('Analytics tracking failed:', error); + } } };