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
9 changes: 7 additions & 2 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
}
Expand Down
20 changes: 16 additions & 4 deletions src/components/home/StatsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,42 @@ interface StatsCardProps {
totalMonthlySpend: number;
totalActive: number;
onWalletPress: () => void;
currency?: string;
}


export const StatsCard: React.FC<StatsCardProps> = ({
totalMonthlySpend,
totalActive,
onWalletPress,
currency = 'USD',
}) => {

return (
<View style={styles.container} accessibilityRole="summary">
{/* Monthly Spend Card - Primary Focus */}
<View
style={[styles.card, styles.primaryCard]}
accessible={true}
accessibilityLabel={`Total monthly spend: ${formatCurrencyCompact(totalMonthlySpend)}`}>
<Text style={styles.label} accessibilityElementsHidden={true}>
accessibilityLabel={`Total monthly spend, ${formatCurrencyCompact(
totalMonthlySpend,
currency
)}`}>
<Text
style={styles.statLabel}
accessibilityElementsHidden={true}
importantForAccessibility="no">
Monthly Spend
</Text>
<Text
style={[styles.value, styles.primaryValue]}
numberOfLines={1}
adjustsFontSizeToFit
accessibilityElementsHidden={true}>
{formatCurrencyCompact(totalMonthlySpend)}
accessibilityElementsHidden={true}
importantForAccessibility="no">
{formatCurrencyCompact(totalMonthlySpend, currency)}
</Text>

</View>

{/* Active Count Card */}
Expand Down
34 changes: 29 additions & 5 deletions src/components/subscription/SubscriptionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -37,6 +40,16 @@ export const SubscriptionCard: React.FC<SubscriptionCardProps> = 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 (
<TouchableOpacity
Expand Down Expand Up @@ -93,20 +106,24 @@ export const SubscriptionCard: React.FC<SubscriptionCardProps> = React.memo(
<View
accessible={true}
accessibilityLabel={`Price ${formatCurrency(
subscription.price,
subscription.currency
convertedPrice,
preferredCurrency
)} per ${formatBillingCycle(subscription.billingCycle)}`}
style={styles.priceContainer}>
<Text style={styles.price}>
{formatCurrency(subscription.price, subscription.currency)}
</Text>
<Text style={styles.price}>{formatCurrency(convertedPrice, preferredCurrency)}</Text>
{subscription.currency !== preferredCurrency && (
<Text style={styles.originalPrice}>
({formatCurrency(subscription.price, subscription.currency)})
</Text>
)}
<Text
style={[
styles.billingCycle,
{ color: getBillingCycleColor(subscription.billingCycle) },
]}>
/{formatBillingCycle(subscription.billingCycle)}
</Text>

</View>

<View style={styles.billingInfo}>
Expand Down Expand Up @@ -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',
},
Expand Down
40 changes: 35 additions & 5 deletions src/screens/AddSubscriptionScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -28,14 +28,15 @@ interface AddSubscriptionFormData extends SubscriptionFormData {
const AddSubscriptionScreen: React.FC = () => {
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const { addSubscription, isLoading, error } = useSubscriptionStore();
const { preferredCurrency } = useSettingsStore();

const [formData, setFormData] = useState<AddSubscriptionFormData>({
name: '',
description: '',
category: SubscriptionCategory.OTHER,
price: 0,
priceError: '',
currency: 'USD',
currency: preferredCurrency,
billingCycle: BillingCycle.MONTHLY,
nextBillingDate: new Date(),
notificationsEnabled: true,
Expand All @@ -44,6 +45,7 @@ const AddSubscriptionScreen: React.FC = () => {
cryptoAmount: undefined,
});


useEffect(() => {
if (error) {
Alert.alert('Error', error.userMessage);
Expand Down Expand Up @@ -267,8 +269,9 @@ const AddSubscriptionScreen: React.FC = () => {
<View style={styles.inputGroup}>
<Text style={styles.label}>Price *</Text>
<View style={styles.priceInputContainer}>
<Text style={styles.currencySymbol}>$</Text>
<Text style={styles.currencySymbol}>{getCurrencySymbol(formData.currency)}</Text>
<TextInput

style={styles.priceInput}
value={formData.price > 0 ? formData.price.toString() : ''}
onChangeText={(text) => {
Expand Down Expand Up @@ -304,6 +307,33 @@ const AddSubscriptionScreen: React.FC = () => {
) : null}
</View>

<View style={styles.inputGroup}>
<Text style={styles.label}>Currency</Text>
<View style={[styles.categoryGrid, { marginTop: spacing.sm }]}>
{['USD', 'EUR', 'GBP', 'JPY', 'CAD', 'AUD', 'INR'].map((currency) => (
<TouchableOpacity
key={currency}
style={[
styles.categoryItem,
formData.currency === currency && styles.categoryItemSelected,
]}
onPress={() => handleInputChange('currency', currency)}
accessibilityRole="radio"
accessibilityLabel={currency}
accessibilityState={{ checked: formData.currency === currency }}>
<Text
style={[
styles.categoryText,
formData.currency === currency && styles.categoryTextSelected,
]}>
{currency}
</Text>
</TouchableOpacity>
))}
</View>
</View>


<View style={styles.inputGroup}>
<Text style={styles.label}>Next Billing Date *</Text>
<TouchableOpacity
Expand Down
46 changes: 36 additions & 10 deletions src/screens/AnalyticsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import { colors, spacing, typography, borderRadius } from '../utils/constants';
import { useSubscriptionStore } from '../store';
import { SubscriptionCategory, BillingCycle } from '../types/subscription';
import { Card } from '../components/common/Card';
import { useSettingsStore } from '../store/settingsStore';
import { currencyService } from '../services/currencyService';
import { formatCurrency } from '../utils/formatting';


const { width: screenWidth } = Dimensions.get('window');
const CHART_WIDTH = screenWidth - spacing.xl * 2;
Expand All @@ -21,11 +25,14 @@ type DateRange = 'week' | 'month' | 'year';

const AnalyticsScreen: React.FC = () => {
const { subscriptions, stats, calculateStats } = useSubscriptionStore();
const { preferredCurrency, exchangeRates } = useSettingsStore();
const rates = exchangeRates?.rates || {};
const [dateRange, setDateRange] = useState<DateRange>('month');

useEffect(() => {
calculateStats();
}, [subscriptions, calculateStats]);
}, [subscriptions, calculateStats, preferredCurrency, exchangeRates]);


const categoryData = useMemo(() => {
const categories = Object.values(SubscriptionCategory);
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -173,8 +187,9 @@ const AnalyticsScreen: React.FC = () => {
style={styles.summaryValue}
accessibilityElementsHidden={true}
importantForAccessibility="no">
${stats.totalMonthlySpend.toFixed(2)}
{formatCurrency(stats.totalMonthlySpend, preferredCurrency)}
</Text>

</Card>
<Card style={styles.summaryCard}>
<Text
Expand All @@ -187,8 +202,9 @@ const AnalyticsScreen: React.FC = () => {
style={styles.summaryValue}
accessibilityElementsHidden={true}
importantForAccessibility="no">
${stats.totalYearlySpend.toFixed(2)}
{formatCurrency(stats.totalYearlySpend, preferredCurrency)}
</Text>

</Card>
</View>
<Card style={styles.chartCard}>
Expand Down Expand Up @@ -242,8 +258,9 @@ const AnalyticsScreen: React.FC = () => {
fontSize={10}
fill={colors.text}
textAnchor="middle">
${data.amount.toFixed(0)}
{formatCurrency(data.amount, preferredCurrency)}
</SvgText>

)}
</G>
);
Expand Down Expand Up @@ -286,16 +303,25 @@ const AnalyticsScreen: React.FC = () => {
<Text style={styles.chartTitle}>Upcoming Renewals</Text>
<View style={styles.projectionItem}>
<Text style={styles.projectionLabel}>Next 30 Days</Text>
<Text style={styles.projectionValue}>${stats.totalMonthlySpend.toFixed(2)}</Text>
<Text style={styles.projectionValue}>
{formatCurrency(stats.totalMonthlySpend, preferredCurrency)}
</Text>
</View>

<View style={styles.projectionItem}>
<Text style={styles.projectionLabel}>Next 90 Days</Text>
<Text style={styles.projectionValue}>${(stats.totalMonthlySpend * 3).toFixed(2)}</Text>
<Text style={styles.projectionValue}>
{formatCurrency(stats.totalMonthlySpend * 3, preferredCurrency)}
</Text>
</View>

<View style={[styles.projectionItem, styles.projectionItemLast]}>
<Text style={styles.projectionLabel}>Next 12 Months</Text>
<Text style={styles.projectionValue}>${stats.totalYearlySpend.toFixed(2)}</Text>
<Text style={styles.projectionValue}>
{formatCurrency(stats.totalYearlySpend, preferredCurrency)}
</Text>
</View>

</Card>
</ScrollView>
</SafeAreaView>
Expand Down
12 changes: 9 additions & 3 deletions src/screens/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<Subscription[]>([]);
const [showFilterModal, setShowFilterModal] = useState(false);


// Use the new hook
const { filters, filteredAndSorted, activeFilterCount, hasActiveFilters, clearAllFilters } =
useFilteredSubscriptions(subscriptions);

Expand All @@ -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);
Expand Down Expand Up @@ -133,8 +137,10 @@ const HomeScreen: React.FC = () => {
totalMonthlySpend={stats.totalMonthlySpend}
totalActive={stats.totalActive}
onWalletPress={() => navigation.navigate('WalletConnect')}
currency={preferredCurrency}
/>


{!isOnline && (
<View style={styles.offlineBanner}>
<Text style={styles.offlineText}>
Expand Down
Loading
Loading