From 16724d7e069a7fd454fe126c0f498f1ea811c054 Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Thu, 7 May 2026 14:42:47 -0800 Subject: [PATCH 01/10] fix(nodes): drop stale H_PAD reference in PendingCosigns PR #29 removed the H_PAD constant when moving PendingCosigns into WalletScreen's grid (parent now owns horizontal padding) but left one reference at line 57. Result: upstream/v3 fails tsc on a fresh clone. Drop the stale paddingHorizontal entry from the wrap View style array and consolidate the two duplicate @expo/vector-icons imports while in the file. --- mobile_app/components/nodes/PendingCosigns.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mobile_app/components/nodes/PendingCosigns.tsx b/mobile_app/components/nodes/PendingCosigns.tsx index 15cdb1a7..6dc19656 100644 --- a/mobile_app/components/nodes/PendingCosigns.tsx +++ b/mobile_app/components/nodes/PendingCosigns.tsx @@ -3,8 +3,7 @@ import { NativeScrollEvent, NativeSyntheticEvent, ScrollView, StyleSheet, Text, View, Pressable, useWindowDimensions, } from 'react-native'; -import { Feather } from '@expo/vector-icons'; -import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { Feather, MaterialCommunityIcons } from '@expo/vector-icons'; import { fontFamily, useTheme } from '@/theme'; import { useGlass } from '@/hooks/useGlass'; @@ -54,7 +53,7 @@ export const PendingCosigns = memo(function PendingCosigns({ items, onSign, onRe const isEmpty = items.length === 0; return ( - + {/* Section header */} PENDING CO-SIGNS From ccabc746a51c0dd086ac3447210a7b46c77c3cfc Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Thu, 7 May 2026 17:33:25 -0800 Subject: [PATCH 02/10] feat(wallet): expose per-tx decimals in activity entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renderers need decimals to format SPL amounts with the right precision ceiling. Native SOL keeps the canonical SOL_DECIMALS value; SPL deltas carry the mint's reported decimals through from the parsed token balance entry. No render change in this commit — wires the data through so the activity tile and tx detail can stop assuming SOL precision. --- mobile_app/src/services/walletData.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mobile_app/src/services/walletData.ts b/mobile_app/src/services/walletData.ts index 6ffc8971..675815ac 100644 --- a/mobile_app/src/services/walletData.ts +++ b/mobile_app/src/services/walletData.ts @@ -134,6 +134,7 @@ export interface ActivityEntry { amountBaseUnits: string; amountLamports?: number; amountSol: number; + decimals: number; symbol: string; mintAddress?: string; counterparty: string; @@ -314,6 +315,7 @@ function toActivity( status: failed ? "Failed" : "Settled", amountBaseUnits: amountAbs.toString(), amountSol: Number(amountAbs) / Math.pow(10, tokenDelta.decimals), + decimals: tokenDelta.decimals, symbol: tokenDelta.symbol, mintAddress: tokenDelta.mint, counterparty, @@ -336,6 +338,7 @@ function toActivity( amountBaseUnits: String(transfer.lamports), amountLamports: transfer.lamports, amountSol: transfer.lamports / LAMPORTS_PER_SOL, + decimals: SOL_DECIMALS, symbol: "SOL", counterparty, createdAt, From c401155afdf2c4bfb204bc3ccc99fc24ec7283d9 Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Thu, 7 May 2026 17:34:13 -0800 Subject: [PATCH 03/10] fix(wallet): activity tile renders real symbol and decimals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wallet bento's inline activity tile hardcoded the literal 'SOL' under every row and rounded amounts to four decimals regardless of the token. SPL transfers landed on screen labelled as SOL with the wrong precision — every USDC/USDT/BONK/JUP send showed as 'X.XXXX SOL'. Read tx.symbol off the activity entry so the label reflects the actual token, and format the amount via the existing fmtAmount helper using the per-tx decimals from the parser. Native SOL formatting is unchanged since SOL_DECIMALS round-trips through the same path. --- mobile_app/screens/WalletScreen.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mobile_app/screens/WalletScreen.tsx b/mobile_app/screens/WalletScreen.tsx index 9eee6711..58f65711 100644 --- a/mobile_app/screens/WalletScreen.tsx +++ b/mobile_app/screens/WalletScreen.tsx @@ -9,13 +9,14 @@ import { import { SafeAreaView } from 'react-native-safe-area-context'; import { ReceivePanel } from '@/components/wallet/ReceivePanel'; +import { TxDetailModal } from '@/components/wallet/TxDetailModal'; import { PendingCosigns, type PendingCosign } from '@/components/nodes/PendingCosigns'; import { useLxmfContext } from '@/context/LxmfContext'; import { useWallet } from '@/context/WalletContext'; import { useHideBalance } from '@/src/hooks/useHideBalance'; import { useWalletBalance } from '@/src/hooks/useWalletBalance'; import { useNetworkMode } from '@/src/hooks/useNetworkMode'; -import type { TokenBalance } from '@/src/services/walletData'; +import type { ActivityEntry, TokenBalance } from '@/src/services/walletData'; import { fontFamily, useTheme } from '@/theme'; // ── helpers ─────────────────────────────────────────────────────────────────── @@ -194,7 +195,7 @@ function ActivityTile({ refreshing, onRefresh }: { readonly refreshing: boolean; const out = tx.direction === 'send'; const color = out ? '#FF6B6B' : '#14F195'; const sign = out ? '−' : '+'; - const amount = hidden ? '•••' : `${sign}${tx.amountSol.toFixed(4)}`; + const amount = hidden ? '•••' : `${sign}${fmtAmount(tx.amountSol, tx.decimals)}`; const fallback = out ? 'Sent' : 'Received'; const label = tx.counterparty ? `${tx.counterparty.slice(0, 4)}…${tx.counterparty.slice(-4)}` @@ -213,7 +214,7 @@ function ActivityTile({ refreshing, onRefresh }: { readonly refreshing: boolean; {amount} - SOL + {tx.symbol} ); From d3f3318d5c3acf1457ea1071c82b603200573429 Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Thu, 7 May 2026 17:35:22 -0800 Subject: [PATCH 04/10] feat(wallet): tap activity row to open tx detail Wraps each activity tile row in a Pressable that opens the existing TxDetailModal with the full signature, counterparty, slot, fee, memo, and mint when applicable. Light haptic feedback on tap matches the balance refresh and send buttons. Picks up the work that landed TxDetailModal in the wallet bento on the home composition (RecentActivity already tapped through there) and extends the same affordance to the inline tile that the wallet screen actually renders. Reviewer can copy the full tx signature out of the drawer or jump straight to Explorer. --- mobile_app/screens/WalletScreen.tsx | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/mobile_app/screens/WalletScreen.tsx b/mobile_app/screens/WalletScreen.tsx index 58f65711..5c7667e8 100644 --- a/mobile_app/screens/WalletScreen.tsx +++ b/mobile_app/screens/WalletScreen.tsx @@ -159,6 +159,7 @@ function ActivityTile({ refreshing, onRefresh }: { readonly refreshing: boolean; const { colors } = useTheme(); const { hidden } = useHideBalance(); const { activity, activityLoading, activityError, lastFetched } = useWalletBalance(); + const [selectedTx, setSelectedTx] = useState(null); const initialLoad = activityLoading && lastFetched === null; return ( @@ -201,9 +202,19 @@ function ActivityTile({ refreshing, onRefresh }: { readonly refreshing: boolean; ? `${tx.counterparty.slice(0, 4)}…${tx.counterparty.slice(-4)}` : fallback; return ( - { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {}); + setSelectedTx(tx); + }} + style={({ pressed }) => [ + S.activityRow, + i < activity.length - 1 && { borderBottomWidth: 0.5, borderBottomColor: colors.borderSubtle }, + pressed && { opacity: 0.6 }, + ]} + accessibilityRole="button" + accessibilityLabel={`${out ? 'Sent' : 'Received'} ${fmtAmount(tx.amountSol, tx.decimals)} ${tx.symbol}. Tap for transaction details.`} > @@ -216,10 +227,15 @@ function ActivityTile({ refreshing, onRefresh }: { readonly refreshing: boolean; {amount} {tx.symbol} - + ); })} + setSelectedTx(null)} + /> ); } From 3b9a3591d17ab4dc61245142ac954c8a3ef1529e Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Thu, 7 May 2026 18:19:50 -0800 Subject: [PATCH 05/10] fix(wallet): redact activity row a11y label when balance hidden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When balance hide is on, the row text shows ••• but the accessibilityLabel still spelled out the real amount and symbol, leaking the hidden balance to screen readers and a11y tooling. Conditionally render a redacted label when hidden. --- mobile_app/screens/WalletScreen.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mobile_app/screens/WalletScreen.tsx b/mobile_app/screens/WalletScreen.tsx index 5c7667e8..80983aac 100644 --- a/mobile_app/screens/WalletScreen.tsx +++ b/mobile_app/screens/WalletScreen.tsx @@ -214,7 +214,11 @@ function ActivityTile({ refreshing, onRefresh }: { readonly refreshing: boolean; pressed && { opacity: 0.6 }, ]} accessibilityRole="button" - accessibilityLabel={`${out ? 'Sent' : 'Received'} ${fmtAmount(tx.amountSol, tx.decimals)} ${tx.symbol}. Tap for transaction details.`} + accessibilityLabel={ + hidden + ? `${out ? 'Sent' : 'Received'} transaction, amount hidden. Tap for transaction details.` + : `${out ? 'Sent' : 'Received'} ${fmtAmount(tx.amountSol, tx.decimals)} ${tx.symbol}. Tap for transaction details.` + } > From 8349632637b42c60f9b7ed4b58a6e5467895a1c5 Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Thu, 7 May 2026 18:49:13 -0800 Subject: [PATCH 06/10] fix(wallet): TxDetailModal close+explorer buttons fire; copy rows show check feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues caught on Seeker smoke: 1. Close and Explorer DepthButtons silently swallowed taps. Layout had a Pressable absoluteFill sibling to the SafeAreaView sheet — on Android that absoluteFill intercepted touches that should have hit the nested DepthButton Pressables inside the sheet. Refactored: the backdrop is now itself a Pressable (onPress=onClose), the sheet is a nested Pressable that absorbs taps with onPress=noop so they don't bubble up and trigger dismiss. Inner DepthButtons receive presses normally. 2. Copy buttons on detail rows had no feedback — tapping a sig/memo/mint row copied silently and the user had no signal it worked. Added per- row copied state with 1400ms timeout, swapping the icon copy→check and tinting label/icon colors.primary, matching the pattern used in ReceivePanel.tsx for the wallet address copy. --- .../components/wallet/TxDetailModal.tsx | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/mobile_app/components/wallet/TxDetailModal.tsx b/mobile_app/components/wallet/TxDetailModal.tsx index d356735e..457d72d0 100644 --- a/mobile_app/components/wallet/TxDetailModal.tsx +++ b/mobile_app/components/wallet/TxDetailModal.tsx @@ -1,6 +1,6 @@ import * as Clipboard from "expo-clipboard"; import * as WebBrowser from "expo-web-browser"; -import React from "react"; +import React, { useState } from "react"; import { Modal, Pressable, ScrollView, StyleSheet, Text, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; @@ -10,6 +10,8 @@ import * as haptics from "@/src/design-system/haptics"; import { buildDevnetExplorerTxUrl } from "@/src/services/explorer"; import { fontFamily as FF, useTheme } from "@/theme"; +const COPY_FEEDBACK_MS = 1400; + interface TxDetailModalProps { readonly tx: ActivityEntry | null; readonly visible: boolean; @@ -45,13 +47,22 @@ function DetailRow({ readonly copyValue?: string; }) { const { colors } = useTheme(); + const [copied, setCopied] = useState(false); async function handleCopy() { if (!copyValue) return; haptics.tap(); - await Clipboard.setStringAsync(copyValue); + try { + await Clipboard.setStringAsync(copyValue); + setCopied(true); + setTimeout(() => setCopied(false), COPY_FEEDBACK_MS); + } catch { + setCopied(false); + } } + const iconColor = copied ? colors.primary : colors.textTertiary; + return ( {label} - + {value} - {copyValue ? : null} + {copyValue ? : null} ); @@ -83,9 +94,17 @@ export function TxDetailModal({ tx, visible, onClose }: TxDetailModalProps) { return ( - - - + + {/* Inner Pressable absorbs taps on the sheet so they don't bubble up + to the backdrop and trigger dismiss. Previous implementation used + an absoluteFill Pressable as a sibling to the sheet, which on + Android intercepted taps to DepthButton's nested Pressable inside + the sheet (close/explorer buttons silently swallowed). */} + undefined} + style={[S.sheet, { backgroundColor: colors.surface0, borderColor: colors.borderStrong }]} + > + @@ -125,7 +144,8 @@ export function TxDetailModal({ tx, visible, onClose }: TxDetailModalProps) { - + + ); } From 028e1d04f629707a4e565f2321073a11ad4b458e Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Thu, 7 May 2026 19:05:31 -0800 Subject: [PATCH 07/10] =?UTF-8?q?fix(wallet):=20TxDetailModal=20buttons=20?= =?UTF-8?q?fire=20=E2=80=94=20split=20dismiss=20area=20from=20sheet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous fix (8349632) wrapped the sheet in a noop Pressable to absorb taps. On Android the parent Pressable still claimed the responder before nested DepthButton Pressables could activate, so close/explorer remained dead. Switched to the canonical bottom-sheet pattern: dismissArea is a flex:1 Pressable that occupies only the space above the sheet (column flex with justifyContent:flex-end pushes the sheet to bottom; the dismiss area takes the remaining space). The sheet itself is a plain SafeAreaView with no Pressable wrapper, so DepthButton's onPress receives presses directly without any parent responder competition. Tap-on-backdrop still dismisses (dismissArea fills everything above the sheet); tap-on-sheet does not bubble (no overlap). --- .../components/wallet/TxDetailModal.tsx | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/mobile_app/components/wallet/TxDetailModal.tsx b/mobile_app/components/wallet/TxDetailModal.tsx index 457d72d0..af5215e3 100644 --- a/mobile_app/components/wallet/TxDetailModal.tsx +++ b/mobile_app/components/wallet/TxDetailModal.tsx @@ -94,17 +94,16 @@ export function TxDetailModal({ tx, visible, onClose }: TxDetailModalProps) { return ( - - {/* Inner Pressable absorbs taps on the sheet so they don't bubble up - to the backdrop and trigger dismiss. Previous implementation used - an absoluteFill Pressable as a sibling to the sheet, which on - Android intercepted taps to DepthButton's nested Pressable inside - the sheet (close/explorer buttons silently swallowed). */} - undefined} + {/* Dismiss-Pressable fills only the space above the sheet (flex:1 inside + a flex-end column) so it never overlaps the sheet area. Sheet is a + plain SafeAreaView — no parent Pressable to claim the responder + before nested DepthButton presses register. */} + + + - @@ -144,18 +143,20 @@ export function TxDetailModal({ tx, visible, onClose }: TxDetailModalProps) { - - + ); } const S = StyleSheet.create({ - backdrop: { + root: { backgroundColor: "rgba(0,0,0,0.68)", flex: 1, justifyContent: "flex-end", }, + dismissArea: { + flex: 1, + }, sheet: { borderTopLeftRadius: 22, borderTopRightRadius: 22, From 0a9adaa719076f329f0759fc4a2168973e057054 Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Thu, 7 May 2026 19:14:21 -0800 Subject: [PATCH 08/10] fix(wallet): swap DepthButton for plain Pressable in TxDetailModal actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Despite split-pattern layout (028e1d0) DepthButton's onPress still did not fire on Seeker for Close/Explorer. Plain Pressable on detail rows in the same ScrollView fires correctly, so the responder issue is DepthButton-specific in this Modal+ScrollView context — likely the nested Animated.View + LinearGradient stack swallowing the touch. Replaced with two plain Pressables styled to match the visual hierarchy (secondary outline for Explorer, filled primary for Close). Min-height 44, hit-target safe. Pressable feedback via opacity, no animation machinery to interfere with the responder. Also swapped expo-web-browser openBrowserAsync for Linking.openURL, matching SuccessCard's existing handleExplorer pattern. Linking is the RN-native path; one less native module in the touch handler chain. --- .../components/wallet/TxDetailModal.tsx | 54 ++++++++++++++++--- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/mobile_app/components/wallet/TxDetailModal.tsx b/mobile_app/components/wallet/TxDetailModal.tsx index af5215e3..12dd4b4f 100644 --- a/mobile_app/components/wallet/TxDetailModal.tsx +++ b/mobile_app/components/wallet/TxDetailModal.tsx @@ -1,10 +1,9 @@ import * as Clipboard from "expo-clipboard"; -import * as WebBrowser from "expo-web-browser"; import React, { useState } from "react"; -import { Modal, Pressable, ScrollView, StyleSheet, Text, View } from "react-native"; +import { Linking, Modal, Pressable, ScrollView, StyleSheet, Text, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -import { DepthButton, Icon, Pill } from "@/components/primitives"; +import { Icon, Pill } from "@/components/primitives"; import type { ActivityEntry } from "@/src/services/walletData"; import * as haptics from "@/src/design-system/haptics"; import { buildDevnetExplorerTxUrl } from "@/src/services/explorer"; @@ -87,9 +86,9 @@ export function TxDetailModal({ tx, visible, onClose }: TxDetailModalProps) { if (!tx) return null; const activeTx = tx; - async function handleExplorer() { + function handleExplorer() { haptics.tap(); - await WebBrowser.openBrowserAsync(buildDevnetExplorerTxUrl(activeTx.signature)); + Linking.openURL(buildDevnetExplorerTxUrl(activeTx.signature)).catch(() => undefined); } return ( @@ -138,8 +137,32 @@ export function TxDetailModal({ tx, visible, onClose }: TxDetailModalProps) { - - + [ + S.actionBtn, + S.actionBtnSecondary, + { borderColor: colors.border, backgroundColor: colors.surface1 }, + pressed && { opacity: 0.7 }, + ]} + > + Explorer + + [ + S.actionBtn, + S.actionBtnPrimary, + { backgroundColor: colors.primary }, + pressed && { opacity: 0.85 }, + ]} + > + Close + @@ -243,7 +266,22 @@ const S = StyleSheet.create({ flexDirection: "row", gap: 10, }, - actionButton: { + actionBtn: { + alignItems: "center", + borderRadius: 12, flex: 1, + justifyContent: "center", + minHeight: 44, + paddingHorizontal: 16, + paddingVertical: 12, + }, + actionBtnSecondary: { + borderWidth: 1, + }, + actionBtnPrimary: {}, + actionBtnText: { + fontFamily: FF.sansSb, + fontSize: 14, + letterSpacing: 0.3, }, }); From 099d0391b2289456db95c69ce50086cbf661298a Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Thu, 7 May 2026 19:20:07 -0800 Subject: [PATCH 09/10] =?UTF-8?q?chore(wallet):=20clean=20up=20TxDetailMod?= =?UTF-8?q?al=20=E2=80=94=20drop=20empty=20style,=20refresh=20stale=20comm?= =?UTF-8?q?ent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the DepthButton swap (0a9adaa), the actionBtnPrimary StyleSheet entry was left as {}, and the layout comment still referenced DepthButton. Drop the empty style + its consumer site, rewrite the comment to describe the actual current pattern. --- mobile_app/components/wallet/TxDetailModal.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/mobile_app/components/wallet/TxDetailModal.tsx b/mobile_app/components/wallet/TxDetailModal.tsx index 12dd4b4f..4bae95bc 100644 --- a/mobile_app/components/wallet/TxDetailModal.tsx +++ b/mobile_app/components/wallet/TxDetailModal.tsx @@ -93,10 +93,10 @@ export function TxDetailModal({ tx, visible, onClose }: TxDetailModalProps) { return ( - {/* Dismiss-Pressable fills only the space above the sheet (flex:1 inside - a flex-end column) so it never overlaps the sheet area. Sheet is a - plain SafeAreaView — no parent Pressable to claim the responder - before nested DepthButton presses register. */} + {/* Bottom-sheet split pattern: dismissArea is a flex:1 Pressable that + fills only the space above the sheet (column flex with + justifyContent:flex-end). Sheet has no parent Pressable so nested + action buttons receive presses without responder competition. */} [ S.actionBtn, - S.actionBtnPrimary, { backgroundColor: colors.primary }, pressed && { opacity: 0.85 }, ]} @@ -278,7 +277,6 @@ const S = StyleSheet.create({ actionBtnSecondary: { borderWidth: 1, }, - actionBtnPrimary: {}, actionBtnText: { fontFamily: FF.sansSb, fontSize: 14, From 7bf9c90329a33bc8522c9c91e4a7ca12809781bb Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Fri, 8 May 2026 01:37:01 -0800 Subject: [PATCH 10/10] fix(wallet): clean up TxDetailModal copy-feedback timer + a11y Track the COPY_FEEDBACK_MS reset timer in a ref so: - rapid re-taps clear the stale timer before scheduling a new one (prevents the earlier timer flipping copied back to false mid-window) - the cleanup effect clears it on unmount (prevents the late setCopied call on an unmounted component when the modal closes within 1.4s of a copy) Mark the dismiss-area Pressable as accessible={false}. The visible Close button at the bottom of the sheet is the screen-reader path; an unlabeled backdrop just adds a phantom focusable region. --- .../components/wallet/TxDetailModal.tsx | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/mobile_app/components/wallet/TxDetailModal.tsx b/mobile_app/components/wallet/TxDetailModal.tsx index 4bae95bc..70fc6209 100644 --- a/mobile_app/components/wallet/TxDetailModal.tsx +++ b/mobile_app/components/wallet/TxDetailModal.tsx @@ -1,5 +1,5 @@ import * as Clipboard from "expo-clipboard"; -import React, { useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { Linking, Modal, Pressable, ScrollView, StyleSheet, Text, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; @@ -47,6 +47,11 @@ function DetailRow({ }) { const { colors } = useTheme(); const [copied, setCopied] = useState(false); + const resetTimer = useRef | null>(null); + + useEffect(() => () => { + if (resetTimer.current !== null) clearTimeout(resetTimer.current); + }, []); async function handleCopy() { if (!copyValue) return; @@ -54,7 +59,14 @@ function DetailRow({ try { await Clipboard.setStringAsync(copyValue); setCopied(true); - setTimeout(() => setCopied(false), COPY_FEEDBACK_MS); + // Clear any in-flight reset before scheduling the next one. Without this, + // rapid re-taps stack timers and an earlier one can flip `copied` back to + // false before the latest tap's window finishes. + if (resetTimer.current !== null) clearTimeout(resetTimer.current); + resetTimer.current = setTimeout(() => { + resetTimer.current = null; + setCopied(false); + }, COPY_FEEDBACK_MS); } catch { setCopied(false); } @@ -98,7 +110,12 @@ export function TxDetailModal({ tx, visible, onClose }: TxDetailModalProps) { justifyContent:flex-end). Sheet has no parent Pressable so nested action buttons receive presses without responder competition. */} - +