diff --git a/App.tsx b/App.tsx index 8a72e7a..f08cd6b 100644 --- a/App.tsx +++ b/App.tsx @@ -16,9 +16,10 @@ import '@walletconnect/react-native-compat'; import { createAppKit, defaultConfig, AppKit } from '@reown/appkit-ethers-react-native'; import { EVM_RPC_URLS } from './src/config/evm'; -import { useNetworkStore } from './src/store'; +import { useNetworkStore, useSettingsStore } from './src/store'; import { sessionService } from './src/services/auth/session'; + // Get projectId from environment variable const projectId = process.env.WALLET_CONNECT_PROJECT_ID || 'YOUR_PROJECT_ID'; @@ -76,10 +77,14 @@ function NotificationBootstrap() { useTransactionQueue(); const { initialize } = useNetworkStore(); + const { initializeSettings } = useSettingsStore(); + React.useEffect(() => { initialize(); + void initializeSettings(); void sessionService.initializeCurrentSession(); - }, [initialize]); + }, [initialize, initializeSettings]); + return null; } diff --git a/src/components/home/StatsCard.tsx b/src/components/home/StatsCard.tsx index df6e1e2..867f290 100644 --- a/src/components/home/StatsCard.tsx +++ b/src/components/home/StatsCard.tsx @@ -7,30 +7,42 @@ interface StatsCardProps { totalMonthlySpend: number; totalActive: number; onWalletPress: () => void; + currency?: string; } + export const StatsCard: React.FC = ({ totalMonthlySpend, totalActive, onWalletPress, + currency = 'USD', }) => { + return ( {/* Monthly Spend Card - Primary Focus */} - + accessibilityLabel={`Total monthly spend, ${formatCurrencyCompact( + totalMonthlySpend, + currency + )}`}> + Monthly Spend - {formatCurrencyCompact(totalMonthlySpend)} + accessibilityElementsHidden={true} + importantForAccessibility="no"> + {formatCurrencyCompact(totalMonthlySpend, currency)} + {/* Active Count Card */} diff --git a/src/components/subscription/SubscriptionCard.tsx b/src/components/subscription/SubscriptionCard.tsx index 7ce2984..accba0c 100644 --- a/src/components/subscription/SubscriptionCard.tsx +++ b/src/components/subscription/SubscriptionCard.tsx @@ -14,6 +14,9 @@ import { getBillingCycleColor, isUpcomingBilling, } from '../../utils/subscriptionHelpers'; +import { useSettingsStore } from '../../store/settingsStore'; +import { currencyService } from '../../services/currencyService'; + export interface SubscriptionCardProps { subscription: Subscription; @@ -37,6 +40,16 @@ export const SubscriptionCard: React.FC = React.memo( }; const upcoming = isUpcomingBilling(subscription.nextBillingDate); + const { preferredCurrency, exchangeRates } = useSettingsStore(); + const rates = exchangeRates?.rates || {}; + + const convertedPrice = currencyService.convert( + subscription.price, + subscription.currency, + preferredCurrency, + rates + ); + return ( = React.memo( - - {formatCurrency(subscription.price, subscription.currency)} - + {formatCurrency(convertedPrice, preferredCurrency)} + {subscription.currency !== preferredCurrency && ( + + ({formatCurrency(subscription.price, subscription.currency)}) + + )} = React.memo( ]}> /{formatBillingCycle(subscription.billingCycle)} + @@ -227,10 +244,17 @@ const styles = StyleSheet.create({ color: colors.text, fontWeight: 'bold', }, + originalPrice: { + ...typography.caption, + color: colors.textSecondary, + marginLeft: spacing.xs, + alignSelf: 'center', + }, billingCycle: { ...typography.body, marginLeft: spacing.xs, }, + billingInfo: { alignItems: 'flex-end', }, diff --git a/src/screens/AddSubscriptionScreen.tsx b/src/screens/AddSubscriptionScreen.tsx index a76564c..6642b56 100644 --- a/src/screens/AddSubscriptionScreen.tsx +++ b/src/screens/AddSubscriptionScreen.tsx @@ -14,10 +14,10 @@ import { import { useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { RootStackParamList } from '../navigation/types'; -import { colors, spacing, typography, borderRadius } from '../utils/constants'; -import { SubscriptionCategory, BillingCycle, SubscriptionFormData } from '../types/subscription'; -import { useSubscriptionStore } from '../store'; +import { useSubscriptionStore, useSettingsStore } from '../store'; import { Button } from '../components/common/Button'; +import { getCurrencySymbol } from '../utils/formatting'; + import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker'; import { errorHandler } from '../services/errorHandler'; @@ -28,6 +28,7 @@ interface AddSubscriptionFormData extends SubscriptionFormData { const AddSubscriptionScreen: React.FC = () => { const navigation = useNavigation>(); const { addSubscription, isLoading, error } = useSubscriptionStore(); + const { preferredCurrency } = useSettingsStore(); const [formData, setFormData] = useState({ name: '', @@ -35,7 +36,7 @@ const AddSubscriptionScreen: React.FC = () => { category: SubscriptionCategory.OTHER, price: 0, priceError: '', - currency: 'USD', + currency: preferredCurrency, billingCycle: BillingCycle.MONTHLY, nextBillingDate: new Date(), notificationsEnabled: true, @@ -44,6 +45,7 @@ const AddSubscriptionScreen: React.FC = () => { cryptoAmount: undefined, }); + useEffect(() => { if (error) { Alert.alert('Error', error.userMessage); @@ -267,8 +269,9 @@ const AddSubscriptionScreen: React.FC = () => { Price * - $ + {getCurrencySymbol(formData.currency)} 0 ? formData.price.toString() : ''} onChangeText={(text) => { @@ -304,6 +307,33 @@ const AddSubscriptionScreen: React.FC = () => { ) : null} + + Currency + + {['USD', 'EUR', 'GBP', 'JPY', 'CAD', 'AUD', 'INR'].map((currency) => ( + handleInputChange('currency', currency)} + accessibilityRole="radio" + accessibilityLabel={currency} + accessibilityState={{ checked: formData.currency === currency }}> + + {currency} + + + ))} + + + + Next Billing Date * { const { subscriptions, stats, calculateStats } = useSubscriptionStore(); + const { preferredCurrency, exchangeRates } = useSettingsStore(); + const rates = exchangeRates?.rates || {}; const [dateRange, setDateRange] = useState('month'); useEffect(() => { calculateStats(); - }, [subscriptions, calculateStats]); + }, [subscriptions, calculateStats, preferredCurrency, exchangeRates]); + const categoryData = useMemo(() => { const categories = Object.values(SubscriptionCategory); @@ -80,10 +87,17 @@ const AnalyticsScreen: React.FC = () => { const monthIndex = dateRange === 'week' ? Math.floor(createdAt.getDate() / 7) : createdAt.getMonth(); if (dateRange === 'year' || monthIndex === index) { - if (sub.billingCycle === BillingCycle.MONTHLY) total += sub.price; - else if (sub.billingCycle === BillingCycle.YEARLY) total += sub.price / 12; - else if (sub.billingCycle === BillingCycle.WEEKLY) total += sub.price * 4; + const priceInPreferred = currencyService.convert( + sub.price, + sub.currency, + preferredCurrency, + rates + ); + if (sub.billingCycle === BillingCycle.MONTHLY) total += priceInPreferred; + else if (sub.billingCycle === BillingCycle.YEARLY) total += priceInPreferred / 12; + else if (sub.billingCycle === BillingCycle.WEEKLY) total += priceInPreferred * 4; } + } }); return { month, amount: total }; @@ -173,8 +187,9 @@ const AnalyticsScreen: React.FC = () => { style={styles.summaryValue} accessibilityElementsHidden={true} importantForAccessibility="no"> - ${stats.totalMonthlySpend.toFixed(2)} + {formatCurrency(stats.totalMonthlySpend, preferredCurrency)} + { style={styles.summaryValue} accessibilityElementsHidden={true} importantForAccessibility="no"> - ${stats.totalYearlySpend.toFixed(2)} + {formatCurrency(stats.totalYearlySpend, preferredCurrency)} + @@ -242,8 +258,9 @@ const AnalyticsScreen: React.FC = () => { fontSize={10} fill={colors.text} textAnchor="middle"> - ${data.amount.toFixed(0)} + {formatCurrency(data.amount, preferredCurrency)} + )} ); @@ -286,16 +303,25 @@ const AnalyticsScreen: React.FC = () => { Upcoming Renewals Next 30 Days - ${stats.totalMonthlySpend.toFixed(2)} + + {formatCurrency(stats.totalMonthlySpend, preferredCurrency)} + + Next 90 Days - ${(stats.totalMonthlySpend * 3).toFixed(2)} + + {formatCurrency(stats.totalMonthlySpend * 3, preferredCurrency)} + + Next 12 Months - ${stats.totalYearlySpend.toFixed(2)} + + {formatCurrency(stats.totalYearlySpend, preferredCurrency)} + + diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index c4cb4e4..c90116b 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -12,7 +12,8 @@ import { useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { colors, spacing, typography, borderRadius } from '../utils/constants'; -import { useSubscriptionStore } from '../store'; +import { useSubscriptionStore, useSettingsStore } from '../store'; + import { getUpcomingSubscriptions } from '../utils/dummyData'; import { Subscription } from '../types/subscription'; import { RootStackParamList } from '../navigation/types'; @@ -38,11 +39,13 @@ const HomeScreen: React.FC = () => { const isOnline = useTransactionQueueStore((state) => state.isOnline); const pendingTransactions = useTransactionQueueStore((state) => state.queuedTransactions.length); const { level } = useGamificationStore(); - + const { preferredCurrency, exchangeRates } = useSettingsStore(); const [refreshing, setRefreshing] = useState(false); const [upcomingSubscriptions, setUpcomingSubscriptions] = useState([]); const [showFilterModal, setShowFilterModal] = useState(false); + + // Use the new hook const { filters, filteredAndSorted, activeFilterCount, hasActiveFilters, clearAllFilters } = useFilteredSubscriptions(subscriptions); @@ -59,7 +62,8 @@ const HomeScreen: React.FC = () => { useEffect(() => { calculateStats(); if (subscriptions) setUpcomingSubscriptions(getUpcomingSubscriptions(subscriptions)); - }, [subscriptions, calculateStats]); + }, [subscriptions, calculateStats, preferredCurrency, exchangeRates]); + const onRefresh = async () => { setRefreshing(true); @@ -133,8 +137,10 @@ const HomeScreen: React.FC = () => { totalMonthlySpend={stats.totalMonthlySpend} totalActive={stats.totalActive} onWalletPress={() => navigation.navigate('WalletConnect')} + currency={preferredCurrency} /> + {!isOnline && ( diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index a82c1e1..ad32f89 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -12,9 +12,9 @@ import { Modal, FlatList, } from 'react-native'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import { colors, spacing, typography, borderRadius } from '../utils/constants'; -import { useWalletStore, useNetworkStore } from '../store'; +import { useWalletStore, useNetworkStore, useSettingsStore } from '../store'; + import { Card } from '../components/common/Card'; import { useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; @@ -33,44 +33,31 @@ const SettingsScreen: React.FC = () => { const navigation = useNavigation>(); const { address, disconnect } = useWalletStore(); const { currentNetwork, availableNetworks, setNetwork, initialize } = useNetworkStore(); - const [settings, setSettings] = useState({ - notificationsEnabled: true, - defaultCurrency: 'USD', - }); + const { + preferredCurrency, + notificationsEnabled, + setPreferredCurrency, + setNotificationsEnabled, + } = useSettingsStore(); + const [networkModalVisible, setNetworkModalVisible] = useState(false); useEffect(() => { - loadSettings(); initialize(); }, [initialize]); - const loadSettings = async () => { - try { - const savedSettings = await AsyncStorage.getItem(SETTINGS_KEY); - if (savedSettings) setSettings(JSON.parse(savedSettings)); - } catch (error) { - console.error('Failed to load settings:', error); - } - }; - - const saveSettings = async (newSettings: Settings) => { - try { - await AsyncStorage.setItem(SETTINGS_KEY, JSON.stringify(newSettings)); - setSettings(newSettings); - } catch (error) { - console.error('Failed to save settings:', error); - } - }; const handleNotificationToggle = useCallback( - (value: boolean) => saveSettings({ ...settings, notificationsEnabled: value }), - [settings] + (value: boolean) => setNotificationsEnabled(value), + [setNotificationsEnabled] ); + const handleCurrencyChange = useCallback( - (currency: string) => saveSettings({ ...settings, defaultCurrency: currency }), - [settings] + (currency: string) => setPreferredCurrency(currency), + [setPreferredCurrency] ); + const handleDisconnectWallet = useCallback(() => { Alert.alert(t('settings.disconnect_wallet'), t('settings.disconnect_wallet_confirm'), [ { text: t('common.cancel'), style: 'cancel' }, @@ -147,14 +134,15 @@ const SettingsScreen: React.FC = () => { {t('settings.billing_reminders_desc')} + @@ -173,20 +161,21 @@ const SettingsScreen: React.FC = () => { key={currency} style={[ styles.currencyButton, - settings.defaultCurrency === currency && styles.currencyButtonActive, + preferredCurrency === currency && styles.currencyButtonActive, ]} onPress={() => handleCurrencyChange(currency)} accessibilityRole="radio" accessibilityLabel={currency} - accessibilityState={{ checked: settings.defaultCurrency === currency }}> + accessibilityState={{ checked: preferredCurrency === currency }}> {currency} + ))} diff --git a/src/screens/SubscriptionDetailScreen.tsx b/src/screens/SubscriptionDetailScreen.tsx index 38f8d2a..2291d32 100644 --- a/src/screens/SubscriptionDetailScreen.tsx +++ b/src/screens/SubscriptionDetailScreen.tsx @@ -12,11 +12,11 @@ import { } from 'react-native'; import { useNavigation, useRoute, RouteProp } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; - -import { colors, spacing, typography } from '../utils/constants'; -import { useSubscriptionStore } from '../store'; +import { useSubscriptionStore, useSettingsStore } from '../store'; +import { currencyService } from '../services/currencyService'; import { formatCurrency } from '../utils/formatting'; -import { SubscriptionCategory } from '../types/subscription'; +import { colors, spacing, typography } from '../utils/constants'; +import { Subscription, SubscriptionCategory } from '../types/subscription'; import { RootStackParamList } from '../navigation/types'; // Components @@ -32,7 +32,16 @@ const SubscriptionDetailScreen: React.FC = () => { const route = useRoute(); const { id } = route.params; - const { subscriptions, toggleSubscriptionStatus, updateSubscription } = useSubscriptionStore(); + const { + subscriptions, + toggleSubscriptionStatus, + deleteSubscription, + updateSubscription, + recordBillingOutcome, + } = useSubscriptionStore(); + const { preferredCurrency, exchangeRates } = useSettingsStore(); + const rates = exchangeRates?.rates || {}; + const subscription = useMemo(() => subscriptions?.find((s) => s.id === id), [id, subscriptions]); @@ -113,7 +122,115 @@ const SubscriptionDetailScreen: React.FC = () => { {/* Header */} - navigation.goBack()} style={styles.backButton}> + navigation.goBack()} + accessibilityRole="button" + accessibilityLabel="Go back" + hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}> + + + + Subscription Details + + + + + {/* Main Info Card */} + + + {getCategoryIcon(subscription.category)} + + + {subscription.name} + + + {subscription.category.charAt(0).toUpperCase() + subscription.category.slice(1)} + + + + + {subscription.description && ( + {subscription.description} + )} + + + {/* Price Card */} + + Pricing + + + Amount + + {formatCurrency( + currencyService.convert( + subscription.price, + subscription.currency, + preferredCurrency, + rates + ), + preferredCurrency + )} + + {subscription.currency !== preferredCurrency && ( + + Original: {formatCurrency(subscription.price, subscription.currency)} + + )} + + + + Billing Cycle + + {subscription.billingCycle.charAt(0).toUpperCase() + + subscription.billingCycle.slice(1)} + + + + + Next Billing Date + + {new Date(subscription.nextBillingDate).toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + })} + + + + + {/* Notifications */} + + Billing notifications + + Renewal reminders (1 day before, or 1 hour if due sooner) and charge alerts + + + Enabled for this subscription + + updateSubscription(subscription.id, { notificationsEnabled: value }) + } + trackColor={{ false: colors.border, true: colors.primary }} + thumbColor={colors.text} + /> + + Test charge alerts (local only) + + void recordBillingOutcome(subscription.id, 'success')} + style={styles.simulateLink} + testID="simulate-charge-success-button"> + Simulate successful charge + + navigation.goBack()} + accessibilityRole="button" + accessibilityLabel="Go back" + hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}> Details @@ -305,6 +422,32 @@ const styles = StyleSheet.create({ padding: spacing.md, alignItems: 'center', }, + priceLabel: { + ...typography.caption, + color: colors.textSecondary, + marginBottom: spacing.xs, + }, + priceValue: { + ...typography.h3, + color: colors.text, + }, + originalPriceDetail: { + ...typography.caption, + color: colors.textSecondary, + marginTop: spacing.xs, + }, + nextBillingRow: { + + borderTopWidth: 1, + borderTopColor: colors.border, + paddingTop: spacing.md, + }, + nextBillingDate: { + ...typography.body, + color: colors.accent, + fontWeight: '600', + marginTop: spacing.xs, + }, marginRight: { marginRight: spacing.sm, }, diff --git a/src/services/currencyService.ts b/src/services/currencyService.ts new file mode 100644 index 0000000..3ec113a --- /dev/null +++ b/src/services/currencyService.ts @@ -0,0 +1,96 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; + +const BASE_URL = 'https://api.frankfurter.dev/v1'; // Update to v1 or v2 as per documentation +const RATES_CACHE_KEY = '@subtrackr_exchange_rates'; +const CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours + +export interface ExchangeRates { + amount: number; + base: string; + date: string; + rates: Record; + timestamp: number; +} + +class CurrencyService { + /** + * Fetch exchange rates from the API + * @param base The base currency (default: USD) + */ + async fetchRates(base: string = 'USD'): Promise { + try { + const response = await fetch(`${BASE_URL}/latest?from=${base}`); + if (!response.ok) { + throw new Error(`Failed to fetch rates: ${response.statusText}`); + } + const data = await response.json(); + const result: ExchangeRates = { + ...data, + timestamp: Date.now(), + }; + + // Cache the rates + await AsyncStorage.setItem(RATES_CACHE_KEY, JSON.stringify(result)); + return result; + } catch (error) { + console.error('CurrencyService fetchRates error:', error); + return this.getCachedRates(); + } + } + + /** + * Get cached rates from AsyncStorage + */ + async getCachedRates(): Promise { + try { + const cached = await AsyncStorage.getItem(RATES_CACHE_KEY); + if (cached) { + return JSON.parse(cached); + } + } catch (error) { + console.error('CurrencyService getCachedRates error:', error); + } + return null; + } + + /** + * Convert an amount from one currency to another + * @param amount The value to convert + * @param from Origin currency code + * @param to Target currency code + * @param rates Current exchange rates (relative to a base, usually USD) + */ + convert( + amount: number, + from: string, + to: string, + rates: Record, + base: string = 'USD' + ): number { + if (from === to) return amount; + + // Convert to base first + let amountInBase = amount; + if (from !== base) { + const rateFromBase = rates[from]; + if (!rateFromBase) return amount; // Fallback to original if rate missing + amountInBase = amount / rateFromBase; + } + + // Convert from base to target + if (to === base) return amountInBase; + const rateToBase = rates[to]; + if (!rateToBase) return amountInBase; // Fallback to base if rate missing + + return amountInBase * rateToBase; + } + + /** + * Check if cached rates are expired + */ + isCacheExpired(timestamp: number): boolean { + return Date.now() - timestamp > CACHE_EXPIRY; + } +} + +export const currencyService = new CurrencyService(); diff --git a/src/store/index.ts b/src/store/index.ts index 608012e..c357267 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -5,3 +5,5 @@ export { useWalletStore } from './walletStore'; export { useNetworkStore } from './networkStore'; export { useCommunityStore } from './communityStore'; export { useAccountingStore } from './accountingStore'; +export { useSettingsStore } from './settingsStore'; + diff --git a/src/store/settingsStore.ts b/src/store/settingsStore.ts new file mode 100644 index 0000000..136a138 --- /dev/null +++ b/src/store/settingsStore.ts @@ -0,0 +1,54 @@ +import { create } from 'zustand'; +import { persist, createJSONStorage } from 'zustand/middleware'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { currencyService, ExchangeRates } from '../services/currencyService'; + +interface SettingsState { + preferredCurrency: string; + notificationsEnabled: boolean; + exchangeRates: ExchangeRates | null; + isLoading: boolean; + + // Actions + setPreferredCurrency: (currency: string) => void; + setNotificationsEnabled: (enabled: boolean) => void; + updateExchangeRates: () => Promise; + initializeSettings: () => Promise; +} + +export const useSettingsStore = create()( + persist( + (set, get) => ({ + preferredCurrency: 'USD', + notificationsEnabled: true, + exchangeRates: null, + isLoading: false, + + setPreferredCurrency: (currency) => { + set({ preferredCurrency: currency }); + // Optionally update rates immediately if base changed, + // but here we keep USD as base for rates to simplify conversion + void get().updateExchangeRates(); + }, + + setNotificationsEnabled: (enabled) => set({ notificationsEnabled: enabled }), + + updateExchangeRates: async () => { + set({ isLoading: true }); + const rates = await currencyService.fetchRates('USD'); + set({ exchangeRates: rates, isLoading: false }); + }, + + initializeSettings: async () => { + const { exchangeRates } = get(); + if (!exchangeRates || currencyService.isCacheExpired(exchangeRates.timestamp)) { + await get().updateExchangeRates(); + } + }, + }), + { + name: 'subtrackr-settings-store', + storage: createJSONStorage(() => AsyncStorage), + } + ) +); diff --git a/src/store/subscriptionStore.ts b/src/store/subscriptionStore.ts index b314482..f90572f 100644 --- a/src/store/subscriptionStore.ts +++ b/src/store/subscriptionStore.ts @@ -21,6 +21,9 @@ import { useGamificationStore } from './gamificationStore'; import { useInvoiceStore } from './invoiceStore'; import { AchievementTrigger } from '../types/gamification'; import { errorHandler, AppError } from '../services/errorHandler'; +import { useSettingsStore } from './settingsStore'; +import { currencyService } from '../services/currencyService'; + const STORAGE_KEY = 'subtrackr-subscriptions'; const STORE_VERSION = 1; @@ -365,23 +368,39 @@ export const useSubscriptionStore = create()( const activeSubs = subscriptions.filter((sub) => sub.isActive); + const { preferredCurrency, exchangeRates } = useSettingsStore.getState(); + const rates = exchangeRates?.rates || {}; + const totalMonthlySpend = activeSubs.reduce((total, sub) => { - if (sub.billingCycle === 'monthly') return total + sub.price; - if (sub.billingCycle === 'yearly') return total + sub.price / 12; + const priceInPreferred = currencyService.convert( + sub.price, + sub.currency, + preferredCurrency, + rates + ); + if (sub.billingCycle === 'monthly') return total + priceInPreferred; + if (sub.billingCycle === 'yearly') return total + priceInPreferred / 12; if (sub.billingCycle === 'weekly') - return total + sub.price * BILLING_CONVERSIONS.WEEKS_PER_MONTH; - return total + sub.price; + return total + priceInPreferred * BILLING_CONVERSIONS.WEEKS_PER_MONTH; + return total + priceInPreferred; }, 0); const totalYearlySpend = activeSubs.reduce((total, sub) => { - if (sub.billingCycle === 'yearly') return total + sub.price; + const priceInPreferred = currencyService.convert( + sub.price, + sub.currency, + preferredCurrency, + rates + ); + if (sub.billingCycle === 'yearly') return total + priceInPreferred; if (sub.billingCycle === 'monthly') - return total + sub.price * BILLING_CONVERSIONS.MONTHS_PER_YEAR; + return total + priceInPreferred * BILLING_CONVERSIONS.MONTHS_PER_YEAR; if (sub.billingCycle === 'weekly') - return total + sub.price * BILLING_CONVERSIONS.WEEKS_PER_YEAR; - return total + sub.price * BILLING_CONVERSIONS.MONTHS_PER_YEAR; + return total + priceInPreferred * BILLING_CONVERSIONS.WEEKS_PER_YEAR; + return total + priceInPreferred * BILLING_CONVERSIONS.MONTHS_PER_YEAR; }, 0); + const categoryBreakdown = activeSubs.reduce( (acc, sub) => { acc[sub.category] = (acc[sub.category] || 0) + 1; diff --git a/src/utils/formatting.ts b/src/utils/formatting.ts index c008120..9978a0f 100644 --- a/src/utils/formatting.ts +++ b/src/utils/formatting.ts @@ -100,3 +100,19 @@ export const truncateText = (text: string, maxLength: number): string => { if (text.length <= maxLength) return text; return text.slice(0, maxLength) + '...'; }; + +export const getCurrencySymbol = (currency: string): string => { + const symbols: Record = { + USD: '$', + EUR: '€', + GBP: '£', + JPY: '¥', + CAD: '$', + AUD: '$', + INR: '₹', + BTC: '₿', + ETH: 'Ξ', + }; + return symbols[currency] || currency; +}; +