Skip to content

Commit 48b11b6

Browse files
committed
Implement AsyncStorage for user authentication and balance calculations; refactor FriendsScreen and HomeScreen imports
1 parent b642a27 commit 48b11b6

File tree

6 files changed

+313
-68
lines changed

6 files changed

+313
-68
lines changed

frontend/context/AuthContext.js

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import React, { createContext, useState, useEffect } from 'react';
1+
import AsyncStorage from '@react-native-async-storage/async-storage';
2+
import { createContext, useEffect, useState } from 'react';
23
import * as authApi from '../api/auth';
34

45
export const AuthContext = createContext();
@@ -8,12 +9,61 @@ export const AuthProvider = ({ children }) => {
89
const [token, setToken] = useState(null);
910
const [isLoading, setIsLoading] = useState(true);
1011

11-
// For now, we are not persisting the token. A real app would use AsyncStorage.
12-
// This effect will just simulate checking for a token on app start.
12+
// Load token and user data from AsyncStorage on app start
1313
useEffect(() => {
14-
setIsLoading(false);
14+
const loadStoredAuth = async () => {
15+
try {
16+
const storedToken = await AsyncStorage.getItem('auth_token');
17+
const storedUser = await AsyncStorage.getItem('user_data');
18+
19+
if (storedToken && storedUser) {
20+
setToken(storedToken);
21+
setUser(JSON.parse(storedUser));
22+
}
23+
} catch (error) {
24+
console.error('Failed to load stored authentication:', error);
25+
} finally {
26+
setIsLoading(false);
27+
}
28+
};
29+
30+
loadStoredAuth();
1531
}, []);
1632

33+
// Save token to AsyncStorage whenever it changes
34+
useEffect(() => {
35+
const saveToken = async () => {
36+
try {
37+
if (token) {
38+
await AsyncStorage.setItem('auth_token', token);
39+
} else {
40+
await AsyncStorage.removeItem('auth_token');
41+
}
42+
} catch (error) {
43+
console.error('Failed to save token to storage:', error);
44+
}
45+
};
46+
47+
saveToken();
48+
}, [token]);
49+
50+
// Save user data to AsyncStorage whenever it changes
51+
useEffect(() => {
52+
const saveUser = async () => {
53+
try {
54+
if (user) {
55+
await AsyncStorage.setItem('user_data', JSON.stringify(user));
56+
} else {
57+
await AsyncStorage.removeItem('user_data');
58+
}
59+
} catch (error) {
60+
console.error('Failed to save user data to storage:', error);
61+
}
62+
};
63+
64+
saveUser();
65+
}, [user]);
66+
1767
const login = async (email, password) => {
1868
try {
1969
const response = await authApi.login(email, password);
@@ -37,7 +87,15 @@ export const AuthProvider = ({ children }) => {
3787
}
3888
};
3989

40-
const logout = () => {
90+
const logout = async () => {
91+
try {
92+
// Clear stored authentication data
93+
await AsyncStorage.removeItem('auth_token');
94+
await AsyncStorage.removeItem('user_data');
95+
} catch (error) {
96+
console.error('Failed to clear stored authentication:', error);
97+
}
98+
4199
setToken(null);
42100
setUser(null);
43101
};

frontend/package-lock.json

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,21 @@
99
"web": "expo start --web"
1010
},
1111
"dependencies": {
12+
"@expo/metro-runtime": "~5.0.4",
13+
"@react-native-async-storage/async-storage": "^2.2.0",
1214
"@react-navigation/bottom-tabs": "^7.4.4",
1315
"@react-navigation/native": "^7.1.16",
1416
"@react-navigation/native-stack": "^7.3.23",
1517
"axios": "^1.11.0",
1618
"expo": "~53.0.20",
1719
"expo-status-bar": "~2.2.3",
1820
"react": "19.0.0",
21+
"react-dom": "19.0.0",
1922
"react-native": "0.79.5",
2023
"react-native-paper": "^5.14.5",
2124
"react-native-safe-area-context": "^5.5.2",
2225
"react-native-screens": "^4.13.1",
23-
"react-dom": "19.0.0",
24-
"react-native-web": "^0.20.0",
25-
"@expo/metro-runtime": "~5.0.4"
26+
"react-native-web": "^0.20.0"
2627
},
2728
"devDependencies": {
2829
"@babel/core": "^7.20.0"

frontend/screens/FriendsScreen.js

Lines changed: 9 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,17 @@
1-
import React, { useState, useEffect, useContext } from 'react';
2-
import { View, StyleSheet, FlatList, Alert } from 'react-native';
3-
import { Text, Appbar, List, Divider, ActivityIndicator } from 'react-native-paper';
4-
import { AuthContext } from '../context/AuthContext';
5-
import { getGroups, getGroupDetails } from '../api/groups';
61
import { useIsFocused } from '@react-navigation/native';
2+
import { useContext, useEffect, useState } from 'react';
3+
import { Alert, FlatList, StyleSheet, View } from 'react-native';
4+
import { ActivityIndicator, Appbar, Divider, List, Text } from 'react-native-paper';
5+
import { getGroupDetails, getGroups } from '../api/groups';
6+
import { AuthContext } from '../context/AuthContext';
7+
import { calculateFriendBalances } from '../utils/balanceCalculator';
78

89
const FriendsScreen = () => {
910
const { token, user } = useContext(AuthContext);
1011
const [friends, setFriends] = useState([]);
1112
const [isLoading, setIsLoading] = useState(true);
1213
const isFocused = useIsFocused();
1314

14-
const calculateFriendBalances = (groupsWithDetails) => {
15-
const balances = {}; // { friendId: { name, netBalance, groups: { groupId: { name, balance } } } }
16-
17-
groupsWithDetails.forEach(group => {
18-
const [membersResponse, expensesResponse] = group.details;
19-
const members = membersResponse.data;
20-
const expenses = expensesResponse.data.expenses;
21-
22-
if (!expenses) return; // Guard against undefined expenses
23-
24-
expenses.forEach(expense => {
25-
const payerId = expense.createdBy;
26-
const payerIsMe = payerId === user._id;
27-
28-
expense.splits.forEach(split => {
29-
const memberId = split.userId;
30-
const memberIsMe = memberId === user._id;
31-
32-
if (memberId === payerId) return; // Payer doesn't owe themselves
33-
34-
if (payerIsMe && !memberIsMe) { // I paid, they owe me
35-
if (!balances[memberId]) balances[memberId] = { name: members.find(m => m.userId === memberId)?.user.name || 'Unknown', netBalance: 0, groups: {} };
36-
if (!balances[memberId].groups[group.id]) balances[memberId].groups[group.id] = { name: group.name, balance: 0 };
37-
balances[memberId].netBalance += split.amount;
38-
balances[memberId].groups[group.id].balance += split.amount;
39-
} else if (!payerIsMe && memberIsMe) { // They paid, I owe them
40-
if (!balances[payerId]) balances[payerId] = { name: members.find(m => m.userId === payerId)?.user.name || 'Unknown', netBalance: 0, groups: {} };
41-
if (!balances[payerId].groups[group.id]) balances[payerId].groups[group.id] = { name: group.name, balance: 0 };
42-
balances[payerId].netBalance -= split.amount;
43-
balances[payerId].groups[group.id].balance -= split.amount;
44-
}
45-
});
46-
});
47-
});
48-
49-
// Format the data for the UI
50-
const formattedFriends = Object.entries(balances).map(([id, data]) => ({
51-
id,
52-
name: data.name,
53-
netBalance: data.netBalance,
54-
groups: Object.entries(data.groups).map(([groupId, groupData]) => ({
55-
id: groupId,
56-
name: groupData.name,
57-
balance: groupData.balance,
58-
})),
59-
}));
60-
61-
setFriends(formattedFriends);
62-
};
63-
6415
useEffect(() => {
6516
const fetchData = async () => {
6617
setIsLoading(true);
@@ -75,7 +26,9 @@ const FriendsScreen = () => {
7526
})
7627
);
7728

78-
calculateFriendBalances(groupsWithDetails);
29+
// Use the utility function to calculate friend balances
30+
const calculatedFriends = calculateFriendBalances(groupsWithDetails, user._id);
31+
setFriends(calculatedFriends);
7932

8033
} catch (error) {
8134
console.error('Failed to fetch data for friends screen:', error);

frontend/screens/HomeScreen.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import React, { useState, useEffect, useContext } from 'react';
2-
import { View, StyleSheet, FlatList, Alert } from 'react-native';
3-
import { Button, Text, Card, ActivityIndicator, Appbar, Modal, Portal, TextInput, Avatar } from 'react-native-paper';
1+
import { useContext, useEffect, useState } from 'react';
2+
import { Alert, FlatList, StyleSheet, View } from 'react-native';
3+
import { ActivityIndicator, Appbar, Avatar, Button, Card, Modal, Portal, Text, TextInput } from 'react-native-paper';
4+
import { createGroup, getGroups, getOptimizedSettlements } from '../api/groups';
45
import { AuthContext } from '../context/AuthContext';
5-
import { getGroups, createGroup, getOptimizedSettlements } from '../api/groups';
66

77
const HomeScreen = ({ navigation }) => {
88
const { token, logout, user } = useContext(AuthContext);

0 commit comments

Comments
 (0)