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

### Added
- **Mobile Swipe-to-Delete:** Implemented intuitive swipe gestures for deleting expenses in group details.
- **Features:**
- Swipe left to reveal a delete action (themed with error color).
- Optimistic UI updates to immediately hide the deleted item, preventing flicker.
- "Undo" Snackbar notification allowing users to cancel accidental deletions.
- Robust unmount handling to ensure pending deletions are committed if the user navigates away.
- Haptic feedback on delete triggering.
- **Technical:** Integrated `react-native-gesture-handler` and `react-native-reanimated`. Created `mobile/components/SwipeableExpenseRow.js`. Updated `mobile/screens/GroupDetailsScreen.js`.

- **Password Strength Meter:** Added a visual password strength indicator to the signup form.
- **Features:**
- Real-time strength calculation (Length, Uppercase, Lowercase, Number, Symbol).
Expand Down
25 changes: 23 additions & 2 deletions .Jules/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,29 @@ const clearFieldError = (field: string) => {

## Mobile Patterns

### Swipe-to-Delete with Undo

**Date:** 2026-02-12
**Context:** Implementing expense deletion in GroupDetails

To implement a robust "Swipe to Delete" with Undo capability in React Native:

1. **Dependencies**: Use `react-native-gesture-handler` (with `GestureHandlerRootView` at root) and `react-native-reanimated`.
2. **Optimistic UI**: Update local state *immediately* to remove the item. Do not wait for API. This prevents flickering.
3. **Persistence on Unmount**: Use a `useRef` to track pending deletions. In `useEffect` cleanup, force-commit the deletion if the user navigates away while the "Undo" snackbar is visible.
4. **Theme Integration**: Pass theme colors (e.g., `theme.colors.error`) to the swipeable component for consistency.

```javascript
// Example Cleanup Pattern
useEffect(() => {
return () => {
if (pendingDeleteIdRef.current) {
api.delete(pendingDeleteIdRef.current);
}
};
}, []);
```

### React Native Paper Components

**Date:** 2026-01-01
Expand Down Expand Up @@ -578,8 +601,6 @@ _Document errors and their solutions here as you encounter them._
- Verification requires triggering an error during render (e.g., conditional throw).
- Must wrap Router if using `useNavigate` or `Link` in fallback.

---

### ✅ Successful PR Pattern: Toast Notification System (#227)

**Date:** 2026-01-13
Expand Down
7 changes: 4 additions & 3 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,12 @@

### Mobile

- [ ] **[ux]** Swipe-to-delete for expenses with undo option
- [x] **[ux]** Swipe-to-delete for expenses with undo option
- Completed: 2026-02-12
- File: `mobile/screens/GroupDetailsScreen.js`
- Context: Add swipeable rows with delete action
- Context: Add swipeable rows with delete action and undo snackbar
- Impact: Quick expense management
- Size: ~55 lines
- Size: ~100 lines
- Added: 2026-01-01

- [x] **[style]** Haptic feedback on all button presses
Expand Down
13 changes: 8 additions & 5 deletions mobile/App.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import React from 'react';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import AppNavigator from './navigation/AppNavigator';
import { PaperProvider } from 'react-native-paper';
import { AuthProvider } from './context/AuthContext';

export default function App() {
return (
<AuthProvider>
<PaperProvider>
<AppNavigator />
</PaperProvider>
</AuthProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<AuthProvider>
<PaperProvider>
<AppNavigator />
</PaperProvider>
</AuthProvider>
</GestureHandlerRootView>
);
}
3 changes: 3 additions & 0 deletions mobile/api/groups.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ export const updateMemberRole = (groupId, memberId, role) =>

export const removeMember = (groupId, memberId) =>
apiClient.delete(`/groups/${groupId}/members/${memberId}`);

export const deleteExpense = (groupId, expenseId) =>
apiClient.delete(`/groups/${groupId}/expenses/${expenseId}`);
7 changes: 7 additions & 0 deletions mobile/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['react-native-reanimated/plugin'],
};
};
69 changes: 69 additions & 0 deletions mobile/components/SwipeableExpenseRow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, { useRef } from 'react';
import { View, StyleSheet, Animated } from 'react-native';
import { Swipeable } from 'react-native-gesture-handler';
import { IconButton } from 'react-native-paper';

const SwipeableExpenseRow = ({ children, onDelete, style, deleteColor }) => {
const swipeableRef = useRef(null);

const renderRightActions = (progress, dragX) => {
const scale = dragX.interpolate({
inputRange: [-80, 0],
outputRange: [1, 0],
extrapolate: 'clamp',
});

return (
<View
style={[
styles.rightActionContainer,
{ backgroundColor: deleteColor || "#ff5252" },
]}
>
<Animated.View style={[styles.deleteButton, { transform: [{ scale }] }]}>
<IconButton
icon="delete"
iconColor="white"
size={24}
onPress={() => {
swipeableRef.current?.close();
onDelete();
}}
Comment on lines +28 to +31
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

Add a guard for missing onDelete callback.

If onDelete is not provided (or is undefined), pressing the delete button will throw a TypeError. Consider a defensive check.

🛡️ Proposed fix
             onPress={() => {
                 swipeableRef.current?.close();
-                onDelete();
+                onDelete?.();
             }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onPress={() => {
swipeableRef.current?.close();
onDelete();
}}
onPress={() => {
swipeableRef.current?.close();
onDelete?.();
}}
🤖 Prompt for AI Agents
In `@mobile/components/SwipeableExpenseRow.js` around lines 28 - 31, The onPress
handler currently calls onDelete() unguarded which will throw if onDelete is
undefined; update the onPress in the SwipeableExpenseRow component to first
close the swipeable via swipeableRef.current?.close() and then check that the
onDelete prop is a function (e.g., typeof onDelete === 'function') before
invoking it so the deletion call is skipped safely when the callback is missing.

accessibilityLabel="Delete expense"
accessibilityRole="button"
/>
</Animated.View>
</View>
);
};

return (
<Swipeable
ref={swipeableRef}
renderRightActions={renderRightActions}
friction={2}
rightThreshold={40}
containerStyle={style}
>
{children}
</Swipeable>
);
};

const styles = StyleSheet.create({
rightActionContainer: {
width: 80,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#ff5252',
borderTopRightRadius: 12,
borderBottomRightRadius: 12,
},
Comment on lines +53 to +61
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Redundant backgroundColor in StyleSheet.

Line 58 sets backgroundColor: '#ff5252' in styles.rightActionContainer, but it's always overridden by the inline style on Line 20 ({ backgroundColor: deleteColor || "#ff5252" }). Remove it from the StyleSheet to avoid confusion.

♻️ Proposed fix
   rightActionContainer: {
     width: 80,
     justifyContent: 'center',
     alignItems: 'center',
-    backgroundColor: '#ff5252',
     borderTopRightRadius: 12,
     borderBottomRightRadius: 12,
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const styles = StyleSheet.create({
rightActionContainer: {
width: 80,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#ff5252',
borderTopRightRadius: 12,
borderBottomRightRadius: 12,
},
const styles = StyleSheet.create({
rightActionContainer: {
width: 80,
justifyContent: 'center',
alignItems: 'center',
borderTopRightRadius: 12,
borderBottomRightRadius: 12,
},
🤖 Prompt for AI Agents
In `@mobile/components/SwipeableExpenseRow.js` around lines 53 - 61, Remove the
redundant backgroundColor from the StyleSheet definition for
rightActionContainer: delete the backgroundColor: '#ff5252' entry in
styles.rightActionContainer so the inline prop-controlled color ({
backgroundColor: deleteColor || "#ff5252" } in SwipeableExpenseRow) is the
single source of truth; update styles.rightActionContainer accordingly and
ensure no other parts reference the removed key.

deleteButton: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});

export default SwipeableExpenseRow;
154 changes: 154 additions & 0 deletions mobile/package-lock.json

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

2 changes: 2 additions & 0 deletions mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-gesture-handler": "~2.28.0",
"react-native-paper": "^5.14.5",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "^5.4.0",
"react-native-screens": "^4.11.1",
"react-native-web": "^0.21.0"
Expand Down
Loading
Loading