Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions mobile_app/components/nodes/PendingCosigns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -54,7 +53,7 @@ export const PendingCosigns = memo(function PendingCosigns({ items, onSign, onRe
const isEmpty = items.length === 0;

return (
<View style={[S.wrap, { paddingHorizontal: H_PAD }]}>
<View style={S.wrap}>
{/* Section header */}
<View style={S.labelRow}>
<Text style={[S.sectionLabel, { color: colors.textTertiary }]}>PENDING CO-SIGNS</Text>
Expand Down
106 changes: 90 additions & 16 deletions mobile_app/components/wallet/TxDetailModal.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import * as Clipboard from "expo-clipboard";
import * as WebBrowser from "expo-web-browser";
import React from "react";
import { Modal, Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
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";

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";
import { fontFamily as FF, useTheme } from "@/theme";

const COPY_FEEDBACK_MS = 1400;

interface TxDetailModalProps {
readonly tx: ActivityEntry | null;
readonly visible: boolean;
Expand Down Expand Up @@ -45,13 +46,34 @@ function DetailRow({
readonly copyValue?: string;
}) {
const { colors } = useTheme();
const [copied, setCopied] = useState(false);
const resetTimer = useRef<ReturnType<typeof setTimeout> | null>(null);

useEffect(() => () => {
if (resetTimer.current !== null) clearTimeout(resetTimer.current);
}, []);

Comment thread
epicexcelsior marked this conversation as resolved.
async function handleCopy() {
if (!copyValue) return;
haptics.tap();
await Clipboard.setStringAsync(copyValue);
try {
await Clipboard.setStringAsync(copyValue);
setCopied(true);
// 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);
}
Comment thread
epicexcelsior marked this conversation as resolved.
Comment thread
epicexcelsior marked this conversation as resolved.
}

const iconColor = copied ? colors.primary : colors.textTertiary;

return (
<Pressable
accessibilityLabel={copyValue ? `Copy ${label}` : undefined}
Expand All @@ -62,10 +84,10 @@ function DetailRow({
>
<Text style={[S.detailLabel, { color: colors.textTertiary }]}>{label}</Text>
<View style={S.detailValueWrap}>
<Text numberOfLines={2} style={[S.detailValue, { color: colors.textPrimary }]}>
<Text numberOfLines={2} style={[S.detailValue, { color: copied ? colors.primary : colors.textPrimary }]}>
{value}
</Text>
{copyValue ? <Icon name="copy" size={13} color={colors.textTertiary} /> : null}
{copyValue ? <Icon name={copied ? "check" : "copy"} size={13} color={iconColor} /> : null}
</View>
</Pressable>
);
Expand All @@ -76,16 +98,28 @@ 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 (
<Modal animationType="slide" transparent visible={visible} onRequestClose={onClose}>
<View style={S.backdrop}>
<Pressable style={StyleSheet.absoluteFill} onPress={onClose} />
<SafeAreaView edges={["bottom"]} style={[S.sheet, { backgroundColor: colors.surface0, borderColor: colors.borderStrong }]}>
{/* 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. */}
<View style={S.root}>
<Pressable
accessible={false}
importantForAccessibility="no"
style={S.dismissArea}
onPress={onClose}
/>
<SafeAreaView
edges={["bottom"]}
style={[S.sheet, { backgroundColor: colors.surface0, borderColor: colors.borderStrong }]}
>
<View style={S.handleWrap}>
<View style={[S.handle, { backgroundColor: colors.textTertiary }]} />
</View>
Expand Down Expand Up @@ -120,8 +154,31 @@ export function TxDetailModal({ tx, visible, onClose }: TxDetailModalProps) {
</View>

<View style={S.actions}>
<DepthButton label="Explorer" onPress={handleExplorer} size="md" tone="cyan" variant="secondary" style={S.actionButton} />
<DepthButton label="Close" onPress={onClose} size="md" tone="cyan" variant="primary" style={S.actionButton} />
<Pressable
accessibilityRole="button"
accessibilityLabel="Open in Explorer"
onPress={handleExplorer}
style={({ pressed }) => [
S.actionBtn,
S.actionBtnSecondary,
{ borderColor: colors.border, backgroundColor: colors.surface1 },
pressed && { opacity: 0.7 },
]}
>
<Text style={[S.actionBtnText, { color: colors.textPrimary }]}>Explorer</Text>
</Pressable>
<Pressable
accessibilityRole="button"
accessibilityLabel="Close"
onPress={onClose}
style={({ pressed }) => [
S.actionBtn,
{ backgroundColor: colors.primary },
pressed && { opacity: 0.85 },
]}
>
<Text style={[S.actionBtnText, { color: "#08080A" }]}>Close</Text>
</Pressable>
</View>
</ScrollView>
</SafeAreaView>
Expand All @@ -131,11 +188,14 @@ 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,
Expand Down Expand Up @@ -222,7 +282,21 @@ 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,
},
actionBtnText: {
fontFamily: FF.sansSb,
fontSize: 14,
letterSpacing: 0.3,
},
});
33 changes: 27 additions & 6 deletions mobile_app/screens/WalletScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ───────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -158,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<ActivityEntry | null>(null);
const initialLoad = activityLoading && lastFetched === null;

return (
Expand Down Expand Up @@ -194,15 +196,29 @@ 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)}`
: fallback;
return (
<View
<Pressable
key={tx.signature}
style={[S.activityRow, i < activity.length - 1 && { borderBottomWidth: 0.5, borderBottomColor: colors.borderSubtle }]}
onPress={() => {
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={
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.`
}
>
<View style={[S.activityIconWrap, { backgroundColor: color + '18' }]}>
<Feather name={out ? 'arrow-up-right' : 'arrow-down-left'} size={14} color={color} />
Expand All @@ -213,12 +229,17 @@ function ActivityTile({ refreshing, onRefresh }: { readonly refreshing: boolean;
</View>
<View style={{ alignItems: 'flex-end' }}>
<Text style={[S.activityAmount, { color }]}>{amount}</Text>
<Text style={[S.activityTime, { color: colors.textTertiary }]}>SOL</Text>
<Text style={[S.activityTime, { color: colors.textTertiary }]}>{tx.symbol}</Text>
</View>
</View>
</Pressable>
);
})}
</ScrollView>
<TxDetailModal
tx={selectedTx}
visible={selectedTx !== null}
onClose={() => setSelectedTx(null)}
/>
</View>
);
}
Expand Down
3 changes: 3 additions & 0 deletions mobile_app/src/services/walletData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export interface ActivityEntry {
amountBaseUnits: string;
amountLamports?: number;
amountSol: number;
decimals: number;
symbol: string;
mintAddress?: string;
counterparty: string;
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down