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
43 changes: 26 additions & 17 deletions app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
import { GradientBackground, GradientCard } from '@/components/GradientBackground';
import { LiquidGlassView } from '@/components/LiquidGlassView';
import BalanceChart from '@/components/PriceChart';
import { HomeScreenSkeleton, TransactionListSkeleton } from '@/components/SkeletonLoader';

Check warning on line 5 in app/(tabs)/index.tsx

View workflow job for this annotation

GitHub Actions / ESLint

'TransactionListSkeleton' is defined but never used
import TransactionItem from '@/components/TransactionItem';
import WalletCard from '@/components/WalletCard';
import { createButtonStyle, platformStyles } from '@/constants/themes';
import { WALLET_COLOR_PALETTE } from '@/constants/wallet-colors';
import { useTabAnimation } from '@/hooks/use-tab-animation';
import { useWallet } from '@/hooks/wallet-store';
import { HapticService } from '@/services/haptic-service';
import { Wallet } from '@/types/wallet';
import { isIOS26OrHigher } from '@/utils/platform';
import { LinearGradient } from 'expo-linear-gradient';
Expand Down Expand Up @@ -96,10 +98,11 @@
const carouselRef = useRef<FlatList<CarouselItem>>(null);

const onRefresh = useCallback(async () => {
HapticService.medium();
setRefreshing(true);
await refreshData();
HapticService.success();
setRefreshing(false);
// Track usage when user refreshes data
incrementUsageCount('data_refresh');
}, [refreshData, incrementUsageCount]);

Expand Down Expand Up @@ -232,20 +235,15 @@
);
}, [theme.colors.surface, theme.colors.primary, currentWalletId, switchWallet, handleEditWallet, incrementUsageCount]);

// Show loading state while wallet is being loaded
// Show skeleton loading state while wallet is being loaded
if (isLoading) {
return (
<GradientBackground theme={theme} variant="primary" direction="vertical">
<SafeAreaView style={styles.container}>
<Stack.Screen options={{ title: 'Wallet', headerShown: false }} />
<View style={styles.emptyState}>
<Text style={[styles.emptyTitle, { color: theme.colors.text }]}>
Loading Wallet...
</Text>
<Text style={[styles.emptyText, { color: theme.colors.textSecondary }]}>
Please wait while we load your wallet
</Text>
</View>
<ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent}>
<HomeScreenSkeleton />
</ScrollView>
</SafeAreaView>
</GradientBackground>
);
Expand Down Expand Up @@ -403,9 +401,10 @@
) : (
<>
<View style={styles.balanceContainer}>
<TouchableOpacity
<TouchableOpacity
style={[styles.eyeButton, { backgroundColor: theme.colors.background }]}
onPress={() => {
HapticService.light();
setHideBalanceSetting(!hideBalance);
incrementUsageCount('settings_interaction');
}}
Expand Down Expand Up @@ -463,6 +462,7 @@
},
]}
onPress={() => {
HapticService.light();
setSelectedPeriod(period);
incrementUsageCount('settings_interaction');
}}
Expand Down Expand Up @@ -492,23 +492,31 @@

{/* Action Buttons */}
<View style={styles.actionButtons}>
<TouchableOpacity
<TouchableOpacity
style={[
createButtonStyle(theme, 'primary'),
styles.sendButton,
]}
onPress={() => router.push('/(tabs)/send')}
onPress={() => {
HapticService.medium();
router.push('/(tabs)/send');
}}
activeOpacity={0.85}
>
<ArrowUpRight color="white" size={20} />
<Text style={styles.actionButtonText}>Send</Text>
</TouchableOpacity>
<TouchableOpacity

<TouchableOpacity
style={[
createButtonStyle(theme, 'secondary'),
styles.receiveButton,
]}
onPress={() => router.push('/(tabs)/receive')}
onPress={() => {
HapticService.medium();
router.push('/(tabs)/receive');
}}
activeOpacity={0.85}
>
<ArrowDownLeft color={theme.colors.text} size={20} />
<Text style={[styles.receiveButtonText, { color: theme.colors.text }]}>Receive</Text>
Expand Down Expand Up @@ -559,10 +567,11 @@
</Text>
</View>
) : (
transactions.slice(0, 5).map((transaction: any) => (
transactions.slice(0, 5).map((transaction: any, idx: number) => (
<TransactionItem
key={transaction.txid}
transaction={transaction}
index={idx}
/>
))
)}
Expand Down
26 changes: 14 additions & 12 deletions app/(tabs)/receive.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { GradientBackground } from '@/components/GradientBackground';
import { toast } from '@/components/Toast';
import WalletSelector from '@/components/WalletSelector';
import { ADDRESS_GENERATION_COOLDOWN_MS, GAP_LIMIT_WARNING_THRESHOLD } from '@/constants/cache';
import { createButtonStyle, platformStyles } from '@/constants/themes';
import { useTabAnimation } from '@/hooks/use-tab-animation';
import { useWallet } from '@/hooks/wallet-store';
import { HapticService } from '@/services/haptic-service';
import { loadWalletService } from '@/utils/wallet-service-loader';
import * as Clipboard from 'expo-clipboard';
import { Stack, router } from 'expo-router';
Expand Down Expand Up @@ -122,7 +124,7 @@
setIsLoadingAddress(false);
});
}
}, [currentWallet?.xpub, currentWallet?.addresses?.length]);

Check warning on line 127 in app/(tabs)/receive.tsx

View workflow job for this annotation

GitHub Actions / ESLint

React Hook useEffect has a missing dependency: 'currentWallet.addresses'. Either include it or remove the dependency array

// Spin animation for the refresh icon
useEffect(() => {
Expand Down Expand Up @@ -157,7 +159,8 @@
const hasValidAddress = currentAddress.trim().length > 0 && currentAddress !== 'No address available';

const handleNewAddress = async () => {
if (isGeneratingAddress) return; // Prevent multiple simultaneous requests
if (isGeneratingAddress) return;
HapticService.medium();

// Rate limiting: Prevent spam and API abuse
const now = Date.now();
Expand Down Expand Up @@ -220,43 +223,42 @@
const handleCopy = async () => {
try {
if (!hasValidAddress) {
Alert.alert('Error', 'No address available to copy');
toast.error('No address available to copy');
return;
}

await Clipboard.setStringAsync(currentAddress);
Alert.alert('Copied', 'Address copied to clipboard');
toast.success('Copied!', 'Address copied to clipboard');
} catch (error) {
console.error('Error copying address:', error);
Alert.alert('Error', 'Failed to copy address to clipboard');
toast.error('Copy failed', 'Could not copy address to clipboard');
}
};

const handleShare = async () => {
try {
if (!hasValidAddress) {
Alert.alert('Error', 'No address available to share');
toast.error('No address available to share');
return;
}
// Native sharing

HapticService.light();
await Share.share({
message: `Bitcoin Address: ${currentAddress}`,
title: 'Bitcoin Address',
});
} catch (error) {
console.error('Error sharing:', error);
// Fallback to copy
try {
if (hasValidAddress) {
await Clipboard.setStringAsync(currentAddress);
Alert.alert('Copied', 'Address copied to clipboard instead');
toast.info('Copied instead', 'Address copied to clipboard');
} else {
Alert.alert('Error', 'No address available');
toast.error('No address available');
}
} catch (clipboardError) {
console.error('Clipboard error:', clipboardError);
Alert.alert('Error', 'Unable to share or copy address');
toast.error('Share failed', 'Unable to share or copy address');
}
}
};
Expand Down
8 changes: 8 additions & 0 deletions app/(tabs)/send.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GradientBackground } from '@/components/GradientBackground';
import { LiquidGlassView } from '@/components/LiquidGlassView';
import QRScanner from '@/components/QRScanner';
import { toast } from '@/components/Toast';

Check warning on line 4 in app/(tabs)/send.tsx

View workflow job for this annotation

GitHub Actions / ESLint

'toast' is defined but never used
import { ThemedSwitch } from '@/components/ThemedSwitch';
import WalletSelector from '@/components/WalletSelector';
import { createButtonStyle, createInputStyle, platformStyles } from '@/constants/themes';
Expand All @@ -9,6 +10,7 @@
import { useWallet } from '@/hooks/wallet-store';
import { isValidBitcoinAddress, sendTransaction } from '@/services/bitcoin-service';
import { feeEstimationService } from '@/services/fee-service';
import { HapticService } from '@/services/haptic-service';
import type { UTXO } from '@/types/wallet';
import { Stack, router } from 'expo-router';
import { AlertCircle, ArrowUpRight, CheckCircle, ChevronRight, Coins, QrCode } from 'lucide-react-native';
Expand Down Expand Up @@ -106,6 +108,7 @@

// Memoize handlers to prevent recreation on every render
const handleFeePresetChange = useCallback((preset: 'slow' | 'normal' | 'fast' | 'custom') => {
HapticService.light();
console.log(`🔧 Send screen: Fee preset changed to ${preset}`);
console.log(`🔧 Current feeSettings.defaultPreset: ${feeSettings?.defaultPreset}`);

Expand Down Expand Up @@ -353,6 +356,7 @@
}, [amount, feeRate]);

const handleSendMax = useCallback(() => {
HapticService.light();
try {
if (balance > 0) {
// Calculate more accurate fee estimate
Expand Down Expand Up @@ -700,6 +704,7 @@
);

console.log('✅ Real Bitcoin transaction sent successfully:', result);
HapticService.transactionSuccess();

// Show success message with transaction details
const feeFiat = bitcoinPrice && bitcoinPrice > 0 ? (result.fee * bitcoinPrice).toFixed(2) : 'N/A';
Expand Down Expand Up @@ -771,6 +776,7 @@
}

const errorMessageText = `Failed to send Bitcoin transaction:\n\n${userMessage}\n\nPlease check your inputs and try again.`;
HapticService.transactionError();
Alert.alert(
'Transaction Failed',
errorMessageText,
Expand All @@ -782,9 +788,11 @@
};

const handleReviewTransaction = () => {
HapticService.medium();
try {
// Validate inputs
if (!recipientAddress.trim()) {
HapticService.warning();
Alert.alert('Error', 'Please enter a recipient address');
return;
}
Expand Down
17 changes: 11 additions & 6 deletions app/(tabs)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,11 @@ function SettingsScreenContent() {
logoutScale.value = withSpring(1, { damping: 15, stiffness: 400 });
};

const SettingItem = ({
icon: Icon,
title,
subtitle,
onPress,
const SettingItem = ({
icon: Icon,
title,
subtitle,
onPress,
rightElement,
iconColor = theme.colors.primary,
showDivider = true,
Expand All @@ -177,7 +177,12 @@ function SettingsScreenContent() {
<>
<TouchableOpacity
style={styles.settingItem}
onPress={onPress}
onPress={() => {
if (onPress) {
HapticService.light();
onPress();
}
}}
disabled={!onPress}
activeOpacity={onPress ? 0.7 : 1}
>
Expand Down
5 changes: 4 additions & 1 deletion app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { initializeNetworkingPolyfill } from '../services/networking-polyfill';
import ActivityTracker from '@/components/ActivityTracker';
import PinUnlockScreen from '@/components/PinUnlockScreen';
import SplashScreen from '@/components/SplashScreen';
import { ToastProvider } from '@/components/Toast';
import { platformStyles } from '@/constants/themes';
import { AutoLockProvider, useAutoLock } from '@/hooks/auto-lock-store';
import { useSplashScreen } from '@/hooks/use-splash-screen';
Expand Down Expand Up @@ -353,7 +354,9 @@ function RootLayoutNav() {
<ErrorBoundary key={key}>
<WalletProvider>
<AutoLockProvider>
<AppWithSplash />
<ToastProvider>
<AppWithSplash />
</ToastProvider>
</AutoLockProvider>
</WalletProvider>
</ErrorBoundary>
Expand Down
77 changes: 77 additions & 0 deletions components/AnimatedNumber.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React, { useEffect } from 'react';
import { StyleProp, TextStyle } from 'react-native';

Check warning on line 2 in components/AnimatedNumber.tsx

View workflow job for this annotation

GitHub Actions / ESLint

'/home/runner/work/Wallet/Wallet/node_modules/react-native/index.js' imported multiple times
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming,
useDerivedValue,

Check warning on line 8 in components/AnimatedNumber.tsx

View workflow job for this annotation

GitHub Actions / ESLint

'useDerivedValue' is defined but never used
useAnimatedProps,
runOnJS,

Check warning on line 10 in components/AnimatedNumber.tsx

View workflow job for this annotation

GitHub Actions / ESLint

'runOnJS' is defined but never used
} from 'react-native-reanimated';
import { TextInput } from 'react-native';

Check warning on line 12 in components/AnimatedNumber.tsx

View workflow job for this annotation

GitHub Actions / ESLint

'/home/runner/work/Wallet/Wallet/node_modules/react-native/index.js' imported multiple times

const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);

interface AnimatedNumberProps {
value: number;
formatter?: (value: number) => string;
style?: StyleProp<TextStyle>;
duration?: number;
prefix?: string;
suffix?: string;
}

/**
* AnimatedNumber - Smoothly animates between numeric values with a counting effect.
* Used for balance displays, price tickers, and any numeric value that changes over time.
* Creates a premium feel by making value changes feel alive rather than instant.
*/
export default function AnimatedNumber({
value,
formatter,
style,
duration = 800,
prefix = '',
suffix = '',
}: AnimatedNumberProps) {
const animatedValue = useSharedValue(value);

useEffect(() => {
animatedValue.value = withTiming(value, { duration });
}, [value, duration, animatedValue]);

// Scale animation on value change for emphasis
const scale = useSharedValue(1);

useEffect(() => {
scale.value = withSpring(1.02, { damping: 8, stiffness: 300 }, () => {
scale.value = withSpring(1, { damping: 15, stiffness: 400 });
});
}, [value, scale]);

const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));

const format = formatter || ((v: number) => v.toFixed(2));

const animatedProps = useAnimatedProps(() => {
const text = `${prefix}${format(animatedValue.value)}${suffix}`;
return {
text,
defaultValue: text,
};
});

return (
<Animated.View style={animatedStyle}>
<AnimatedTextInput
editable={false}
underlineColorAndroid="transparent"
style={[{ padding: 0, margin: 0 }, style]}
animatedProps={animatedProps}
/>
</Animated.View>
);
}
Loading
Loading