Skip to content
Open
14 changes: 14 additions & 0 deletions .Jules/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@
## [Unreleased]

### Added
- **Consistent Focus States:** Implemented high-contrast `focus-visible` styles across interactive elements to improve keyboard accessibility.
- **Features:**
- Dual-theme support: Black rings for Neobrutalism, Blue rings for Glassmorphism.
- Applied to `Button` component, Modal close buttons, Toast dismiss buttons, and Auth page actions (Google button, toggle links).
- **Technical:** Used Tailwind's `focus-visible:` modifiers with `ring`, `ring-offset`, and theme-specific colors.

- **Mobile Pull-to-Refresh:** Implemented native pull-to-refresh interactions with haptic feedback for key lists.
- **Features:**
- Integrated `RefreshControl` into `HomeScreen`, `FriendsScreen`, and `GroupDetailsScreen`.
- Added haptic feedback (`Haptics.ImpactFeedbackStyle.Light`) on refresh trigger.
- Separated 'isRefreshing' state from 'isLoading' to prevent full-screen spinner interruptions.
- Themed the refresh spinner using `react-native-paper`'s primary color.
- **Technical:** Installed `expo-haptics`. Refactored data fetching logic to support silent updates.

- **Confirmation Dialog System:** Replaced browser's native `alert`/`confirm` with a custom, accessible, and themed modal system.
- **Features:**
- Dual-theme support (Glassmorphism & Neobrutalism).
Expand Down
20 changes: 11 additions & 9 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@

### Mobile

- [ ] **[ux]** Pull-to-refresh with haptic feedback on all list screens
- Files: `mobile/screens/HomeScreen.js`, `mobile/screens/GroupDetailsScreen.js`
- [x] **[ux]** Pull-to-refresh with haptic feedback on all list screens
- Completed: 2026-01-21
- Files: `mobile/screens/HomeScreen.js`, `mobile/screens/GroupDetailsScreen.js`, `mobile/screens/FriendsScreen.js`
- Context: Add RefreshControl + Expo Haptics to main lists
- Impact: Native feel, users can easily refresh data
- Size: ~45 lines
- Added: 2026-01-01
- Size: ~150 lines
Comment on lines +53 to +58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Task marked complete prematurely—outstanding review comments request changes.

The pull-to-refresh task is marked as completed (2026-01-21), but the PR objectives clearly document unresolved review comments requesting defensive try/catch/finally patterns in the refresh handlers for FriendsScreen.js, GroupDetailsScreen.js, and HomeScreen.js. The review feedback specifically asks for error handling to prevent refresh spinners from remaining active when Haptics.impactAsync rejects.

A task should not be marked complete while there are outstanding change requests on the same files and functionality.

📋 Recommended next steps

Either:

  1. Address the review comments by implementing the defensive error handling patterns, then update the completion date, OR
  2. Revert the completion status to [ ] until the review feedback is resolved and the PR is merged.
🤖 Prompt for AI Agents
In @.Jules/todo.md around lines 53 - 58, The task was marked complete while PR
review requested defensive error handling in the pull-to-refresh handlers;
update each refresh handler (e.g., onRefresh / handleRefresh used with
RefreshControl in the refresh handlers of FriendsScreen, GroupDetailsScreen and
HomeScreen) to wrap the async work and Haptics.impactAsync call in
try/catch/finally so any rejection doesn't leave the spinner active: call
setRefreshing(true) before work, await the refresh logic and Haptics.impactAsync
inside try, log/handle errors in catch, and always call setRefreshing(false) in
finally; after fixing handlers, update the todo entry to either uncheck the task
or change the completion date once the changes are merged.


- [ ] **[ux]** Complete skeleton loading for HomeScreen groups
- File: `mobile/screens/HomeScreen.js`
Expand All @@ -77,12 +77,12 @@

### Web

- [ ] **[style]** Consistent hover/focus states across all buttons
- Files: `web/components/ui/Button.tsx`, usage across pages
- [x] **[style]** Consistent hover/focus states across all buttons
- Files: `web/components/ui/Button.tsx`, `web/components/ui/Modal.tsx`, `web/components/ui/Toast.tsx`, `web/pages/Auth.tsx`
- Context: Ensure all buttons have proper hover + focus-visible styles
- Impact: Professional feel, keyboard users know where they are
- Size: ~35 lines
- Added: 2026-01-01
- Completed: 2026-01-22

### Mobile

Expand Down Expand Up @@ -158,5 +158,7 @@
- Completed: 2026-01-14
- Files modified: `web/components/ErrorBoundary.tsx`, `web/App.tsx`
- Impact: App doesn't crash, users can recover

_No tasks completed yet. Move tasks here after completion._
- [x] **[ux]** Pull-to-refresh with haptic feedback on all list screens
- Completed: 2026-01-21
- Files modified: `mobile/screens/HomeScreen.js`, `mobile/screens/GroupDetailsScreen.js`, `mobile/screens/FriendsScreen.js`
- Impact: Native feel, users can easily refresh data
Comment on lines +162 to +165
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove duplicate task entry.

The pull-to-refresh task appears in both the Mobile high-priority section (lines 53-58) and here in the "Completed Tasks" section. This duplication creates redundancy and maintenance burden.

Consider removing this duplicate entry since the task is already marked as complete in its original location (lines 53-58).

♻️ Suggested cleanup

Since the task is already marked [x] with completion date in the Mobile section (lines 53-58), this duplicate entry in the "Completed Tasks" section can be safely removed.

🤖 Prompt for AI Agents
In @.Jules/todo.md around lines 162 - 165, The TODO contains a duplicated
completed task "Pull-to-refresh with haptic feedback on all list screens";
remove the duplicate entry from the "Completed Tasks" section (the one at lines
162-165) so only the original entry in the Mobile high-priority section remains,
and ensure any associated metadata (completion date and modified file list) is
preserved only in the original Mobile entry to avoid redundancy.

511 changes: 181 additions & 330 deletions backend/tests/expenses/test_expense_service.py

Large diffs are not rendered by default.

23 changes: 10 additions & 13 deletions mobile/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@react-navigation/native-stack": "^7.3.23",
"axios": "^1.11.0",
"expo": "^54.0.25",
"expo-haptics": "~15.0.8",
"expo-image-picker": "~17.0.8",
"expo-status-bar": "~3.0.8",
"react": "19.1.0",
Expand Down
92 changes: 59 additions & 33 deletions mobile/screens/FriendsScreen.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,78 @@
import { useIsFocused } from "@react-navigation/native";
import { useContext, useEffect, useRef, useState } from "react";
import { Alert, Animated, FlatList, StyleSheet, View } from "react-native";
import { Alert, Animated, FlatList, RefreshControl, StyleSheet, View } from "react-native";
import {
Appbar,
Avatar,
Divider,
IconButton,
List,
Text,
useTheme,
} from "react-native-paper";
import * as Haptics from "expo-haptics";
import { getFriendsBalance, getGroups } from "../api/groups";
import { AuthContext } from "../context/AuthContext";
import { formatCurrency } from "../utils/currency";

const FriendsScreen = () => {
const { token, user } = useContext(AuthContext);
const theme = useTheme();
const [friends, setFriends] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [showTooltip, setShowTooltip] = useState(true);
const isFocused = useIsFocused();

useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
// Fetch friends balance + groups concurrently for group icons
const friendsResponse = await getFriendsBalance();
const friendsData = friendsResponse.data.friendsBalance || [];
const groupsResponse = await getGroups();
const groups = groupsResponse?.data?.groups || [];
const groupMeta = new Map(
groups.map((g) => [g._id, { name: g.name, imageUrl: g.imageUrl }])
);
const fetchData = async (showLoading = true) => {
if (showLoading) setIsLoading(true);
try {
// Fetch friends balance + groups concurrently for group icons
const friendsResponse = await getFriendsBalance();
const friendsData = friendsResponse.data.friendsBalance || [];
const groupsResponse = await getGroups();
const groups = groupsResponse?.data?.groups || [];
const groupMeta = new Map(
groups.map((g) => [g._id, { name: g.name, imageUrl: g.imageUrl }])
);

const transformedFriends = friendsData.map((friend) => ({
id: friend.userId,
name: friend.userName,
imageUrl: friend.userImageUrl || null,
netBalance: friend.netBalance,
groups: (friend.breakdown || []).map((group) => ({
id: group.groupId,
name: group.groupName,
balance: group.balance,
imageUrl: groupMeta.get(group.groupId)?.imageUrl || null,
})),
}));
const transformedFriends = friendsData.map((friend) => ({
id: friend.userId,
name: friend.userName,
imageUrl: friend.userImageUrl || null,
netBalance: friend.netBalance,
groups: (friend.breakdown || []).map((group) => ({
id: group.groupId,
name: group.groupName,
balance: group.balance,
imageUrl: groupMeta.get(group.groupId)?.imageUrl || null,
})),
}));

setFriends(transformedFriends);
} catch (error) {
console.error("Failed to fetch friends balance data:", error);
Alert.alert("Error", "Failed to load friends balance data.");
} finally {
setIsLoading(false);
}
};
setFriends(transformedFriends);
} catch (error) {
console.error("Failed to fetch friends balance data:", error);
Alert.alert("Error", "Failed to load friends balance data.");
} finally {
if (showLoading) setIsLoading(false);
}
};

const onRefresh = async () => {
setIsRefreshing(true);
try {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
} catch (error) {
// Ignore haptics errors
}
try {
await fetchData(false);
} finally {
setIsRefreshing(false);
}
};

useEffect(() => {
if (token && isFocused) {
fetchData();
}
Expand Down Expand Up @@ -235,6 +253,14 @@ const FriendsScreen = () => {
ListEmptyComponent={
<Text style={styles.emptyText}>No balances with friends yet.</Text>
}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={onRefresh}
colors={[theme.colors.primary]}
tintColor={theme.colors.primary}
/>
}
/>
</View>
);
Expand Down
Loading
Loading