Skip to content
Draft
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
7 changes: 7 additions & 0 deletions .Jules/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
## [Unreleased]

### Added
- **Mobile Pull-to-Refresh with Haptics:** Implemented native pull-to-refresh functionality for HomeScreen and GroupDetailsScreen.
- **Features:**
- Integrated `expo-haptics` for tactile feedback during refresh.
- Separated `isLoading` (initial load) from `isRefreshing` (pull actions) to prevent content flashing.
- Ensured lists remain visible while refreshing.
- **Technical:** Refactored `fetchGroups` and `fetchData` to accept refresh flags and manage state independently.

- **Error Boundary System:** Implemented a global React Error Boundary to catch render errors gracefully.
- **Features:**
- Dual-theme support (Glassmorphism & Neobrutalism) for the error fallback UI.
Expand Down
38 changes: 38 additions & 0 deletions .Jules/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,44 @@ Commonly used components:

Most screens use `<View style={{ flex: 1 }}>` - consider wrapping in `SafeAreaView` for notched devices.

### Mobile List Refresh Pattern

**Date:** 2026-01-14
**Context:** Implementing Pull-to-Refresh in React Native

When implementing `RefreshControl` on a `FlatList`, you must separate the **initial loading state** from the **refreshing state** to avoid UI flashing.

**Bad Pattern:**
```javascript
// Causes list to unmount and show spinner on refresh
if (isLoading) return <ActivityIndicator />;
<FlatList onRefresh={fetch} refreshing={isLoading} />
```

**Good Pattern:**
```javascript
const [isLoading, setIsLoading] = useState(true); // Initial load
const [isRefreshing, setIsRefreshing] = useState(false); // Pull actions

const fetchData = async (refresh = false) => {
if (refresh) setIsRefreshing(true);
else setIsLoading(true);
// ... fetch ...
setIsLoading(false);
setIsRefreshing(false);
}

// Only show full loader on initial mount
if (isLoading) return <ActivityIndicator />;

return (
<FlatList
onRefresh={() => fetchData(true)}
refreshing={isRefreshing} // List stays visible
/>
);
```

---

## API Response Patterns
Expand Down
10 changes: 4 additions & 6 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,10 @@

### Mobile

- [ ] **[ux]** Pull-to-refresh with haptic feedback on all list screens
- Files: `mobile/screens/HomeScreen.js`, `mobile/screens/GroupDetailsScreen.js`
- Context: Add RefreshControl + Expo Haptics to main lists
- Impact: Native feel, users can easily refresh data
- Size: ~45 lines
- Added: 2026-01-01
- [x] **[ux]** Pull-to-refresh with haptic feedback on all list screens
- Completed: 2026-01-14
- Files modified: `mobile/screens/HomeScreen.js`, `mobile/screens/GroupDetailsScreen.js`
- Impact: Users can now pull to refresh lists with tactile feedback; list remains visible during refresh.

- [ ] **[ux]** Complete skeleton loading for HomeScreen groups
- File: `mobile/screens/HomeScreen.js`
Expand Down
10 changes: 10 additions & 0 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
16 changes: 13 additions & 3 deletions mobile/screens/GroupDetailsScreen.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useContext, useEffect, useState } from "react";
import { Alert, FlatList, StyleSheet, Text, View } from "react-native";
import * as Haptics from "expo-haptics";
import {
ActivityIndicator,
Card,
Expand All @@ -22,16 +23,22 @@ const GroupDetailsScreen = ({ route, navigation }) => {
const [expenses, setExpenses] = useState([]);
const [settlements, setSettlements] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);

// Currency configuration - can be made configurable later
const currency = "₹"; // Default to INR, can be changed to '$' for USD

// Helper function to format currency amounts
const formatCurrency = (amount) => `${currency}${amount.toFixed(2)}`;

const fetchData = async () => {
const fetchData = async (refresh = false) => {
try {
setIsLoading(true);
if (refresh) {
setIsRefreshing(true);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
} else {
setIsLoading(true);
}
// Fetch members, expenses, and settlements in parallel
const [membersResponse, expensesResponse, settlementsResponse] =
await Promise.all([
Expand All @@ -47,6 +54,7 @@ const GroupDetailsScreen = ({ route, navigation }) => {
Alert.alert("Error", "Failed to fetch group details.");
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
};

Expand All @@ -61,7 +69,7 @@ const GroupDetailsScreen = ({ route, navigation }) => {
),
});
if (token && groupId) {
fetchData();
fetchData(false);
}
}, [token, groupId]);

Expand Down Expand Up @@ -202,6 +210,8 @@ const GroupDetailsScreen = ({ route, navigation }) => {
<Text style={styles.emptyText}>No expenses recorded yet.</Text>
}
contentContainerStyle={{ paddingBottom: 80 }} // To avoid FAB overlap
onRefresh={() => fetchData(true)}
refreshing={isRefreshing}
/>

<FAB
Expand Down
25 changes: 18 additions & 7 deletions mobile/screens/HomeScreen.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useContext, useEffect, useState } from "react";
import { Alert, FlatList, StyleSheet, View } from "react-native";
import * as Haptics from "expo-haptics";
import {
ActivityIndicator,
Appbar,
Expand All @@ -19,6 +20,7 @@ const HomeScreen = ({ navigation }) => {
const { token, logout, user } = useContext(AuthContext);
const [groups, setGroups] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [groupSettlements, setGroupSettlements] = useState({}); // Track settlement status for each group

// State for the Create Group modal
Expand Down Expand Up @@ -66,9 +68,15 @@ const HomeScreen = ({ navigation }) => {
}
};

const fetchGroups = async () => {
const fetchGroups = async (refresh = false) => {
try {
setIsLoading(true);
if (refresh) {
setIsRefreshing(true);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
} else {
setIsLoading(true);
}

const response = await getGroups();
const groupsList = response.data.groups;
setGroups(groupsList);
Expand All @@ -92,12 +100,13 @@ const HomeScreen = ({ navigation }) => {
Alert.alert("Error", "Failed to fetch groups.");
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
};

useEffect(() => {
if (token) {
fetchGroups();
fetchGroups(false);
}
}, [token]);

Expand All @@ -111,7 +120,7 @@ const HomeScreen = ({ navigation }) => {
await createGroup(newGroupName);
hideModal();
setNewGroupName("");
await fetchGroups(); // Refresh the groups list
await fetchGroups(true); // Refresh the groups list
} catch (error) {
console.error("Failed to create group:", error);
Alert.alert("Error", "Failed to create group.");
Expand Down Expand Up @@ -226,7 +235,9 @@ const HomeScreen = ({ navigation }) => {
<Appbar.Action
icon="account-plus"
onPress={() =>
navigation.navigate("JoinGroup", { onGroupJoined: fetchGroups })
navigation.navigate("JoinGroup", {
onGroupJoined: () => fetchGroups(true),
})
}
/>
</Appbar.Header>
Expand All @@ -246,8 +257,8 @@ const HomeScreen = ({ navigation }) => {
No groups found. Create or join one!
</Text>
}
onRefresh={fetchGroups}
refreshing={isLoading}
onRefresh={() => fetchGroups(true)}
refreshing={isRefreshing}
/>
)}
</View>
Expand Down
Loading