From b460bed4297b5a2428ec8f876bc119a61c0b6086 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sat, 7 Feb 2026 20:02:42 +0000
Subject: [PATCH 1/4] [jules] style: Add haptic feedback to all mobile buttons
Co-authored-by: Devasy23 <110348311+Devasy23@users.noreply.github.com>
---
.Jules/changelog.md | 7 ++++
.Jules/todo.md | 11 +++---
mobile/components/ui/HapticAppbar.js | 21 ++++++++++++
mobile/components/ui/HapticButton.js | 14 ++++++++
mobile/components/ui/HapticCard.js | 22 ++++++++++++
mobile/components/ui/HapticCheckbox.js | 13 +++++++
mobile/components/ui/HapticFAB.js | 14 ++++++++
mobile/components/ui/HapticIconButton.js | 14 ++++++++
mobile/components/ui/HapticList.js | 24 +++++++++++++
mobile/components/ui/HapticMenu.js | 19 +++++++++++
.../components/ui/HapticSegmentedButtons.js | 13 +++++++
mobile/screens/AccountScreen.js | 11 +++---
mobile/screens/AddExpenseScreen.js | 26 +++++++-------
mobile/screens/EditProfileScreen.js | 14 ++++----
mobile/screens/FriendsScreen.js | 7 ++--
mobile/screens/GroupDetailsScreen.js | 26 +++++++-------
mobile/screens/GroupSettingsScreen.js | 34 +++++++++----------
mobile/screens/HomeScreen.js | 23 +++++++------
mobile/screens/JoinGroupScreen.js | 10 +++---
mobile/screens/LoginScreen.js | 11 +++---
mobile/screens/SignupScreen.js | 11 +++---
mobile/screens/SplitwiseImportScreen.js | 9 ++---
22 files changed, 263 insertions(+), 91 deletions(-)
create mode 100644 mobile/components/ui/HapticAppbar.js
create mode 100644 mobile/components/ui/HapticButton.js
create mode 100644 mobile/components/ui/HapticCard.js
create mode 100644 mobile/components/ui/HapticCheckbox.js
create mode 100644 mobile/components/ui/HapticFAB.js
create mode 100644 mobile/components/ui/HapticIconButton.js
create mode 100644 mobile/components/ui/HapticList.js
create mode 100644 mobile/components/ui/HapticMenu.js
create mode 100644 mobile/components/ui/HapticSegmentedButtons.js
diff --git a/.Jules/changelog.md b/.Jules/changelog.md
index 007d653..7b3396c 100644
--- a/.Jules/changelog.md
+++ b/.Jules/changelog.md
@@ -7,6 +7,13 @@
## [Unreleased]
### Added
+- **Mobile Haptics:** Implemented system-wide haptic feedback for all interactive elements.
+ - **Features:**
+ - Created `HapticButton`, `HapticIconButton`, `HapticFAB`, `HapticCard`, `HapticList`, `HapticCheckbox`, `HapticMenu`, `HapticSegmentedButtons` wrappers.
+ - Integrated into all screens (`Home`, `GroupDetails`, `AddExpense`, `Friends`, `Account`, `EditProfile`, `Login`, `Signup`, `JoinGroup`, `GroupSettings`, `SplitwiseImport`).
+ - Uses `expo-haptics` with `Light` impact style for subtle feedback.
+ - **Technical:** Centralized haptic logic in `mobile/components/ui/` to ensure consistency and maintainability.
+
- **Mobile Accessibility:** Completed accessibility audit for all mobile screens.
- **Features:**
- Added `accessibilityLabel` to all interactive elements (buttons, inputs, list items).
diff --git a/.Jules/todo.md b/.Jules/todo.md
index de49cb8..ccd795e 100644
--- a/.Jules/todo.md
+++ b/.Jules/todo.md
@@ -94,11 +94,12 @@
- Size: ~55 lines
- Added: 2026-01-01
-- [ ] **[style]** Haptic feedback on all button presses
- - Files: All button interactions across mobile
- - Context: Add Expo.Haptics.impactAsync(Light) to buttons
- - Impact: Tactile feedback makes app feel responsive
- - Size: ~40 lines
+- [x] **[style]** Haptic feedback on all button presses
+ - Completed: 2026-02-07
+ - Files: `mobile/components/ui/Haptic*.js`, `mobile/screens/*.js`
+ - Context: Created comprehensive Haptic UI system wrapping React Native Paper components
+ - Impact: Tactile feedback makes app feel responsive and native
+ - Size: ~400 lines
- Added: 2026-01-01
---
diff --git a/mobile/components/ui/HapticAppbar.js b/mobile/components/ui/HapticAppbar.js
new file mode 100644
index 0000000..87fd8df
--- /dev/null
+++ b/mobile/components/ui/HapticAppbar.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import { Appbar } from 'react-native-paper';
+import * as Haptics from 'expo-haptics';
+
+const HapticAppbarAction = ({ onPress, ...props }) => {
+ const handlePress = (e) => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ if (onPress) onPress(e);
+ };
+ return ;
+};
+
+const HapticAppbarBackAction = ({ onPress, ...props }) => {
+ const handlePress = (e) => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ if (onPress) onPress(e);
+ };
+ return ;
+};
+
+export { HapticAppbarAction, HapticAppbarBackAction };
diff --git a/mobile/components/ui/HapticButton.js b/mobile/components/ui/HapticButton.js
new file mode 100644
index 0000000..0776c19
--- /dev/null
+++ b/mobile/components/ui/HapticButton.js
@@ -0,0 +1,14 @@
+import React from 'react';
+import { Button } from 'react-native-paper';
+import * as Haptics from 'expo-haptics';
+
+const HapticButton = ({ onPress, ...props }) => {
+ const handlePress = (e) => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ if (onPress) onPress(e);
+ };
+
+ return ;
+};
+
+export default HapticButton;
diff --git a/mobile/components/ui/HapticCard.js b/mobile/components/ui/HapticCard.js
new file mode 100644
index 0000000..9fa5083
--- /dev/null
+++ b/mobile/components/ui/HapticCard.js
@@ -0,0 +1,22 @@
+import React from 'react';
+import { Card } from 'react-native-paper';
+import * as Haptics from 'expo-haptics';
+
+const HapticCard = ({ onPress, ...props }) => {
+ const handlePress = (e) => {
+ if (onPress) {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ onPress(e);
+ }
+ };
+
+ return ;
+};
+
+// Attach subcomponents
+HapticCard.Content = Card.Content;
+HapticCard.Actions = Card.Actions;
+HapticCard.Cover = Card.Cover;
+HapticCard.Title = Card.Title;
+
+export default HapticCard;
diff --git a/mobile/components/ui/HapticCheckbox.js b/mobile/components/ui/HapticCheckbox.js
new file mode 100644
index 0000000..261695a
--- /dev/null
+++ b/mobile/components/ui/HapticCheckbox.js
@@ -0,0 +1,13 @@
+import React from 'react';
+import { Checkbox } from 'react-native-paper';
+import * as Haptics from 'expo-haptics';
+
+const HapticCheckboxItem = ({ onPress, ...props }) => {
+ const handlePress = (e) => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ if (onPress) onPress(e);
+ };
+ return ;
+};
+
+export default HapticCheckboxItem;
diff --git a/mobile/components/ui/HapticFAB.js b/mobile/components/ui/HapticFAB.js
new file mode 100644
index 0000000..27e81dd
--- /dev/null
+++ b/mobile/components/ui/HapticFAB.js
@@ -0,0 +1,14 @@
+import React from 'react';
+import { FAB } from 'react-native-paper';
+import * as Haptics from 'expo-haptics';
+
+const HapticFAB = ({ onPress, ...props }) => {
+ const handlePress = (e) => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ if (onPress) onPress(e);
+ };
+
+ return ;
+};
+
+export default HapticFAB;
diff --git a/mobile/components/ui/HapticIconButton.js b/mobile/components/ui/HapticIconButton.js
new file mode 100644
index 0000000..ea44b7b
--- /dev/null
+++ b/mobile/components/ui/HapticIconButton.js
@@ -0,0 +1,14 @@
+import React from 'react';
+import { IconButton } from 'react-native-paper';
+import * as Haptics from 'expo-haptics';
+
+const HapticIconButton = ({ onPress, ...props }) => {
+ const handlePress = (e) => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ if (onPress) onPress(e);
+ };
+
+ return ;
+};
+
+export default HapticIconButton;
diff --git a/mobile/components/ui/HapticList.js b/mobile/components/ui/HapticList.js
new file mode 100644
index 0000000..8195204
--- /dev/null
+++ b/mobile/components/ui/HapticList.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import { List } from 'react-native-paper';
+import * as Haptics from 'expo-haptics';
+
+const HapticListItem = ({ onPress, ...props }) => {
+ const handlePress = (e) => {
+ if (onPress) {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ onPress(e);
+ }
+ };
+ return ;
+};
+
+const HapticListAccordion = ({ onPress, ...props }) => {
+ const handlePress = (e) => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ if (onPress) onPress(e);
+ };
+
+ return ;
+};
+
+export { HapticListItem, HapticListAccordion };
diff --git a/mobile/components/ui/HapticMenu.js b/mobile/components/ui/HapticMenu.js
new file mode 100644
index 0000000..de6cd70
--- /dev/null
+++ b/mobile/components/ui/HapticMenu.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import { Menu } from 'react-native-paper';
+import * as Haptics from 'expo-haptics';
+
+const HapticMenuItem = ({ onPress, ...props }) => {
+ const handlePress = (e) => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ if (onPress) onPress(e);
+ };
+ return
;
+};
+
+const HapticMenu = ({ children, ...props }) => {
+ return ;
+};
+
+HapticMenu.Item = HapticMenuItem;
+
+export default HapticMenu;
diff --git a/mobile/components/ui/HapticSegmentedButtons.js b/mobile/components/ui/HapticSegmentedButtons.js
new file mode 100644
index 0000000..28914b9
--- /dev/null
+++ b/mobile/components/ui/HapticSegmentedButtons.js
@@ -0,0 +1,13 @@
+import React from 'react';
+import { SegmentedButtons } from 'react-native-paper';
+import * as Haptics from 'expo-haptics';
+
+const HapticSegmentedButtons = ({ onValueChange, ...props }) => {
+ const handleChange = (value) => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ if (onValueChange) onValueChange(value);
+ };
+ return ;
+};
+
+export default HapticSegmentedButtons;
diff --git a/mobile/screens/AccountScreen.js b/mobile/screens/AccountScreen.js
index c4ed869..ac130a6 100644
--- a/mobile/screens/AccountScreen.js
+++ b/mobile/screens/AccountScreen.js
@@ -1,6 +1,7 @@
import { useContext } from "react";
import { Alert, StyleSheet, View } from "react-native";
import { Appbar, Avatar, Divider, List, Text } from "react-native-paper";
+import { HapticListItem } from '../components/ui/HapticList';
import { AuthContext } from "../context/AuthContext";
const AccountScreen = ({ navigation }) => {
@@ -35,7 +36,7 @@ const AccountScreen = ({ navigation }) => {
- }
onPress={() => navigation.navigate("EditProfile")}
@@ -43,7 +44,7 @@ const AccountScreen = ({ navigation }) => {
accessibilityRole="button"
/>
- }
onPress={handleComingSoon}
@@ -51,7 +52,7 @@ const AccountScreen = ({ navigation }) => {
accessibilityRole="button"
/>
- }
onPress={handleComingSoon}
@@ -59,7 +60,7 @@ const AccountScreen = ({ navigation }) => {
accessibilityRole="button"
/>
- }
onPress={() => navigation.navigate("SplitwiseImport")}
@@ -67,7 +68,7 @@ const AccountScreen = ({ navigation }) => {
accessibilityRole="button"
/>
- }
onPress={handleLogout}
diff --git a/mobile/screens/AddExpenseScreen.js b/mobile/screens/AddExpenseScreen.js
index f5f58a3..958873f 100644
--- a/mobile/screens/AddExpenseScreen.js
+++ b/mobile/screens/AddExpenseScreen.js
@@ -8,15 +8,15 @@ import {
} from "react-native";
import {
ActivityIndicator,
- Button,
- Checkbox,
- Menu,
Paragraph,
- SegmentedButtons,
Text,
TextInput,
Title,
} from "react-native-paper";
+import HapticButton from '../components/ui/HapticButton';
+import HapticCheckboxItem from '../components/ui/HapticCheckbox';
+import HapticMenu from '../components/ui/HapticMenu';
+import HapticSegmentedButtons from '../components/ui/HapticSegmentedButtons';
import { createExpense, getGroupMembers } from "../api/groups";
import { AuthContext } from "../context/AuthContext";
@@ -277,7 +277,7 @@ const AddExpenseScreen = ({ route, navigation }) => {
switch (splitMethod) {
case "equal":
return members.map((member) => (
- {
accessibilityLabel="Expense Amount"
/>
-
+
Split Method
- {
{renderSplitInputs()}
-
+
);
diff --git a/mobile/screens/EditProfileScreen.js b/mobile/screens/EditProfileScreen.js
index 33a8faf..990b8ae 100644
--- a/mobile/screens/EditProfileScreen.js
+++ b/mobile/screens/EditProfileScreen.js
@@ -1,7 +1,9 @@
import * as ImagePicker from "expo-image-picker";
import { useContext, useState } from "react";
import { Alert, StyleSheet, View } from "react-native";
-import { Appbar, Avatar, Button, TextInput, Title } from "react-native-paper";
+import { Appbar, Avatar, TextInput, Title } from "react-native-paper";
+import HapticButton from '../components/ui/HapticButton';
+import { HapticAppbarBackAction } from '../components/ui/HapticAppbar';
import { updateUser } from "../api/auth";
import { AuthContext } from "../context/AuthContext";
@@ -83,7 +85,7 @@ const EditProfileScreen = ({ navigation }) => {
return (
- navigation.goBack()} />
+ navigation.goBack()} />
@@ -98,7 +100,7 @@ const EditProfileScreen = ({ navigation }) => {
) : (
)}
-
+
{
style={styles.input}
accessibilityLabel="Full Name"
/>
-
+
);
diff --git a/mobile/screens/FriendsScreen.js b/mobile/screens/FriendsScreen.js
index ad9bea2..68ce726 100644
--- a/mobile/screens/FriendsScreen.js
+++ b/mobile/screens/FriendsScreen.js
@@ -5,11 +5,12 @@ import {
Appbar,
Avatar,
Divider,
- IconButton,
List,
Text,
useTheme,
} from "react-native-paper";
+import HapticIconButton from '../components/ui/HapticIconButton';
+import { HapticListAccordion } from '../components/ui/HapticList';
import * as Haptics from "expo-haptics";
import { getFriendsBalance, getGroups } from "../api/groups";
import { AuthContext } from "../context/AuthContext";
@@ -94,7 +95,7 @@ const FriendsScreen = () => {
}
return (
- {
all shared groups. Check individual group details for optimized
settlement suggestions.
- setShowTooltip(false)}
diff --git a/mobile/screens/GroupDetailsScreen.js b/mobile/screens/GroupDetailsScreen.js
index bcb0018..7ac1ee8 100644
--- a/mobile/screens/GroupDetailsScreen.js
+++ b/mobile/screens/GroupDetailsScreen.js
@@ -2,13 +2,13 @@ import { useContext, useEffect, useState } from "react";
import { Alert, FlatList, RefreshControl, StyleSheet, Text, View } from "react-native";
import {
ActivityIndicator,
- Card,
- FAB,
- IconButton,
Paragraph,
Title,
useTheme,
} from "react-native-paper";
+import HapticCard from '../components/ui/HapticCard';
+import HapticFAB from '../components/ui/HapticFAB';
+import HapticIconButton from '../components/ui/HapticIconButton';
import * as Haptics from "expo-haptics";
import {
getGroupExpenses,
@@ -65,7 +65,7 @@ const GroupDetailsScreen = ({ route, navigation }) => {
navigation.setOptions({
title: groupName,
headerRight: () => (
- navigation.navigate("GroupSettings", { groupId })}
accessibilityLabel="Group settings"
@@ -103,22 +103,22 @@ const GroupDetailsScreen = ({ route, navigation }) => {
}
return (
-
-
+
{item.description}
Amount: {formatCurrency(item.amount)}
Paid by: {getMemberName(item.paidBy || item.createdBy)}
{balanceText}
-
-
+
+
);
};
@@ -198,12 +198,12 @@ const GroupDetailsScreen = ({ route, navigation }) => {
const renderHeader = () => (
<>
-
-
+
+
Settlement Summary
{renderSettlementSummary()}
-
-
+
+
Expenses
>
@@ -231,7 +231,7 @@ const GroupDetailsScreen = ({ route, navigation }) => {
}
/>
- navigation.navigate("AddExpense", { groupId: groupId })}
diff --git a/mobile/screens/GroupSettingsScreen.js b/mobile/screens/GroupSettingsScreen.js
index f45c099..6dd6111 100644
--- a/mobile/screens/GroupSettingsScreen.js
+++ b/mobile/screens/GroupSettingsScreen.js
@@ -17,13 +17,13 @@ import {
import {
ActivityIndicator,
Avatar,
- Button,
Card,
- IconButton,
- List,
Text,
TextInput,
} from "react-native-paper";
+import HapticButton from '../components/ui/HapticButton';
+import HapticIconButton from '../components/ui/HapticIconButton';
+import { HapticListItem } from '../components/ui/HapticList';
import {
deleteGroup as apiDeleteGroup,
leaveGroup as apiLeaveGroup,
@@ -264,7 +264,7 @@ const GroupSettingsScreen = ({ route, navigation }) => {
const displayName = m.user?.name || "Unknown";
const imageUrl = m.user?.imageUrl;
return (
- {
}
right={() =>
isAdmin && !isSelf ? (
- onKick(m.userId, displayName)}
accessibilityLabel={`Remove ${displayName} from group`}
@@ -315,7 +315,7 @@ const GroupSettingsScreen = ({ route, navigation }) => {
Icon
{ICON_CHOICES.map((i) => (
-
+
))}
-
+
{pickedImage?.uri ? (
{
) : null}
{isAdmin && (
-
+
)}
@@ -382,7 +382,7 @@ const GroupSettingsScreen = ({ route, navigation }) => {
Join Code: {group?.joinCode}
-
+
@@ -398,7 +398,7 @@ const GroupSettingsScreen = ({ route, navigation }) => {
-
+
{isAdmin && (
-
+
)}
diff --git a/mobile/screens/HomeScreen.js b/mobile/screens/HomeScreen.js
index 373bb0a..d2f3c38 100644
--- a/mobile/screens/HomeScreen.js
+++ b/mobile/screens/HomeScreen.js
@@ -4,14 +4,15 @@ import {
ActivityIndicator,
Appbar,
Avatar,
- Button,
- Card,
Modal,
Portal,
Text,
TextInput,
useTheme,
} from "react-native-paper";
+import HapticButton from '../components/ui/HapticButton';
+import HapticCard from '../components/ui/HapticCard';
+import { HapticAppbarAction } from '../components/ui/HapticAppbar';
import * as Haptics from "expo-haptics";
import { createGroup, getGroups, getOptimizedSettlements } from "../api/groups";
import { AuthContext } from "../context/AuthContext";
@@ -176,7 +177,7 @@ const HomeScreen = ({ navigation }) => {
item.imageUrl && /^(https?:|data:image)/.test(item.imageUrl);
const groupIcon = item.imageUrl || item.name?.charAt(0) || "?";
return (
-
navigation.navigate("GroupDetails", {
@@ -189,7 +190,7 @@ const HomeScreen = ({ navigation }) => {
accessibilityLabel={`Group ${item.name}. ${getSettlementStatusText()}`}
accessibilityHint="Double tap to view group details"
>
-
isImage ? (
@@ -199,12 +200,12 @@ const HomeScreen = ({ navigation }) => {
)
}
/>
-
+
{getSettlementStatusText()}
-
-
+
+
);
};
@@ -224,7 +225,7 @@ const HomeScreen = ({ navigation }) => {
style={styles.input}
accessibilityLabel="New group name"
/>
-
+
-
-
navigation.navigate("JoinGroup", { onGroupJoined: fetchGroups })
diff --git a/mobile/screens/JoinGroupScreen.js b/mobile/screens/JoinGroupScreen.js
index a1fc05b..122769d 100644
--- a/mobile/screens/JoinGroupScreen.js
+++ b/mobile/screens/JoinGroupScreen.js
@@ -1,6 +1,8 @@
import { useContext, useState } from "react";
import { Alert, StyleSheet, View } from "react-native";
-import { Appbar, Button, TextInput, Title } from "react-native-paper";
+import { Appbar, TextInput, Title } from "react-native-paper";
+import HapticButton from '../components/ui/HapticButton';
+import { HapticAppbarBackAction } from '../components/ui/HapticAppbar';
import { joinGroup } from "../api/groups";
import { AuthContext } from "../context/AuthContext";
@@ -35,7 +37,7 @@ const JoinGroupScreen = ({ navigation, route }) => {
return (
- navigation.goBack()} />
+ navigation.goBack()} />
@@ -48,7 +50,7 @@ const JoinGroupScreen = ({ navigation, route }) => {
autoCapitalize="characters"
accessibilityLabel="Group Join Code"
/>
-
+
);
diff --git a/mobile/screens/LoginScreen.js b/mobile/screens/LoginScreen.js
index aa5e94a..194db5b 100644
--- a/mobile/screens/LoginScreen.js
+++ b/mobile/screens/LoginScreen.js
@@ -1,6 +1,7 @@
import React, { useState, useContext } from 'react';
import { View, StyleSheet, Alert } from 'react-native';
-import { Button, Text, TextInput } from 'react-native-paper';
+import { Text, TextInput } from 'react-native-paper';
+import HapticButton from '../components/ui/HapticButton';
import { AuthContext } from '../context/AuthContext';
const LoginScreen = ({ navigation }) => {
@@ -42,7 +43,7 @@ const LoginScreen = ({ navigation }) => {
secureTextEntry
accessibilityLabel="Password"
/>
-
-
+
);
};
diff --git a/mobile/screens/SignupScreen.js b/mobile/screens/SignupScreen.js
index c3ba18f..d40f362 100644
--- a/mobile/screens/SignupScreen.js
+++ b/mobile/screens/SignupScreen.js
@@ -1,6 +1,7 @@
import React, { useState, useContext } from 'react';
import { View, StyleSheet, Alert } from 'react-native';
-import { Button, Text, TextInput } from 'react-native-paper';
+import { Text, TextInput } from 'react-native-paper';
+import HapticButton from '../components/ui/HapticButton';
import { AuthContext } from '../context/AuthContext';
const SignupScreen = ({ navigation }) => {
@@ -70,7 +71,7 @@ const SignupScreen = ({ navigation }) => {
secureTextEntry
accessibilityLabel="Confirm Password"
/>
-
-
+
);
};
diff --git a/mobile/screens/SplitwiseImportScreen.js b/mobile/screens/SplitwiseImportScreen.js
index 8abf5cd..d6a65c1 100644
--- a/mobile/screens/SplitwiseImportScreen.js
+++ b/mobile/screens/SplitwiseImportScreen.js
@@ -2,12 +2,13 @@ import { useState } from "react";
import { Alert, Linking, ScrollView, StyleSheet, View } from "react-native";
import {
Appbar,
- Button,
Card,
IconButton,
List,
Text,
} from "react-native-paper";
+import HapticButton from '../components/ui/HapticButton';
+import { HapticAppbarBackAction } from '../components/ui/HapticAppbar';
import { getSplitwiseAuthUrl } from "../api/client";
const SplitwiseImportScreen = ({ navigation }) => {
@@ -45,7 +46,7 @@ const SplitwiseImportScreen = ({ navigation }) => {
return (
- navigation.goBack()} />
+ navigation.goBack()} />
@@ -59,7 +60,7 @@ const SplitwiseImportScreen = ({ navigation }) => {
Import all your friends, groups, and expenses with one click
-
+
You'll be redirected to Splitwise to authorize access
From fb40f6476b8829ff7463d3bebc664fccb33ae537 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sat, 7 Feb 2026 20:07:33 +0000
Subject: [PATCH 2/4] [jules] fix: Fix closing tag syntax error in
FriendsScreen.js
Co-authored-by: Devasy23 <110348311+Devasy23@users.noreply.github.com>
---
mobile/screens/FriendsScreen.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/mobile/screens/FriendsScreen.js b/mobile/screens/FriendsScreen.js
index 68ce726..110fd0d 100644
--- a/mobile/screens/FriendsScreen.js
+++ b/mobile/screens/FriendsScreen.js
@@ -163,7 +163,7 @@ const FriendsScreen = () => {
/>
);
})}
-
+
);
};
From bb920c5955d3654f2275edcc254c8eae06932b69 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sun, 8 Feb 2026 13:00:38 +0000
Subject: [PATCH 3/4] [jules] refactor: Use shared withHapticFeedback HOC
Co-authored-by: Devasy23 <110348311+Devasy23@users.noreply.github.com>
---
.Jules/changelog.md | 2 +-
mobile/components/ui/HapticAppbar.js | 20 +------
mobile/components/ui/HapticButton.js | 12 +---
mobile/components/ui/HapticCard.js | 14 +----
mobile/components/ui/HapticCheckbox.js | 11 +---
mobile/components/ui/HapticFAB.js | 12 +---
mobile/components/ui/HapticIconButton.js | 12 +---
mobile/components/ui/HapticList.js | 23 +-------
mobile/components/ui/HapticMenu.js | 10 +---
.../components/ui/HapticSegmentedButtons.js | 13 ++---
mobile/components/ui/hapticUtils.js | 56 +++++++++++++++++++
mobile/screens/FriendsScreen.js | 4 +-
12 files changed, 81 insertions(+), 108 deletions(-)
create mode 100644 mobile/components/ui/hapticUtils.js
diff --git a/.Jules/changelog.md b/.Jules/changelog.md
index 7b3396c..ecc0f8a 100644
--- a/.Jules/changelog.md
+++ b/.Jules/changelog.md
@@ -9,7 +9,7 @@
### Added
- **Mobile Haptics:** Implemented system-wide haptic feedback for all interactive elements.
- **Features:**
- - Created `HapticButton`, `HapticIconButton`, `HapticFAB`, `HapticCard`, `HapticList`, `HapticCheckbox`, `HapticMenu`, `HapticSegmentedButtons` wrappers.
+ - Created `HapticButton`, `HapticIconButton`, `HapticFAB`, `HapticCard`, `HapticList`, `HapticCheckbox`, `HapticMenu`, `HapticSegmentedButtons`, `HapticAppbar` (including `HapticAppbarAction`, `HapticAppbarBackAction`) wrappers.
- Integrated into all screens (`Home`, `GroupDetails`, `AddExpense`, `Friends`, `Account`, `EditProfile`, `Login`, `Signup`, `JoinGroup`, `GroupSettings`, `SplitwiseImport`).
- Uses `expo-haptics` with `Light` impact style for subtle feedback.
- **Technical:** Centralized haptic logic in `mobile/components/ui/` to ensure consistency and maintainability.
diff --git a/mobile/components/ui/HapticAppbar.js b/mobile/components/ui/HapticAppbar.js
index 87fd8df..b51913b 100644
--- a/mobile/components/ui/HapticAppbar.js
+++ b/mobile/components/ui/HapticAppbar.js
@@ -1,21 +1,7 @@
-import React from 'react';
import { Appbar } from 'react-native-paper';
-import * as Haptics from 'expo-haptics';
+import { withHapticFeedback } from './hapticUtils';
-const HapticAppbarAction = ({ onPress, ...props }) => {
- const handlePress = (e) => {
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
- if (onPress) onPress(e);
- };
- return ;
-};
-
-const HapticAppbarBackAction = ({ onPress, ...props }) => {
- const handlePress = (e) => {
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
- if (onPress) onPress(e);
- };
- return ;
-};
+const HapticAppbarAction = withHapticFeedback(Appbar.Action);
+const HapticAppbarBackAction = withHapticFeedback(Appbar.BackAction);
export { HapticAppbarAction, HapticAppbarBackAction };
diff --git a/mobile/components/ui/HapticButton.js b/mobile/components/ui/HapticButton.js
index 0776c19..f8b821a 100644
--- a/mobile/components/ui/HapticButton.js
+++ b/mobile/components/ui/HapticButton.js
@@ -1,14 +1,6 @@
-import React from 'react';
import { Button } from 'react-native-paper';
-import * as Haptics from 'expo-haptics';
+import { withHapticFeedback } from './hapticUtils';
-const HapticButton = ({ onPress, ...props }) => {
- const handlePress = (e) => {
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
- if (onPress) onPress(e);
- };
-
- return ;
-};
+const HapticButton = withHapticFeedback(Button);
export default HapticButton;
diff --git a/mobile/components/ui/HapticCard.js b/mobile/components/ui/HapticCard.js
index 9fa5083..1d58581 100644
--- a/mobile/components/ui/HapticCard.js
+++ b/mobile/components/ui/HapticCard.js
@@ -1,17 +1,7 @@
-import React from 'react';
import { Card } from 'react-native-paper';
-import * as Haptics from 'expo-haptics';
+import { withHapticFeedback } from './hapticUtils';
-const HapticCard = ({ onPress, ...props }) => {
- const handlePress = (e) => {
- if (onPress) {
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
- onPress(e);
- }
- };
-
- return ;
-};
+const HapticCard = withHapticFeedback(Card, { onlyWhenHandler: true });
// Attach subcomponents
HapticCard.Content = Card.Content;
diff --git a/mobile/components/ui/HapticCheckbox.js b/mobile/components/ui/HapticCheckbox.js
index 261695a..bdb2b87 100644
--- a/mobile/components/ui/HapticCheckbox.js
+++ b/mobile/components/ui/HapticCheckbox.js
@@ -1,13 +1,6 @@
-import React from 'react';
import { Checkbox } from 'react-native-paper';
-import * as Haptics from 'expo-haptics';
+import { withHapticFeedback } from './hapticUtils';
-const HapticCheckboxItem = ({ onPress, ...props }) => {
- const handlePress = (e) => {
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
- if (onPress) onPress(e);
- };
- return ;
-};
+const HapticCheckboxItem = withHapticFeedback(Checkbox.Item);
export default HapticCheckboxItem;
diff --git a/mobile/components/ui/HapticFAB.js b/mobile/components/ui/HapticFAB.js
index 27e81dd..d4d8f49 100644
--- a/mobile/components/ui/HapticFAB.js
+++ b/mobile/components/ui/HapticFAB.js
@@ -1,14 +1,6 @@
-import React from 'react';
import { FAB } from 'react-native-paper';
-import * as Haptics from 'expo-haptics';
+import { withHapticFeedback } from './hapticUtils';
-const HapticFAB = ({ onPress, ...props }) => {
- const handlePress = (e) => {
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
- if (onPress) onPress(e);
- };
-
- return ;
-};
+const HapticFAB = withHapticFeedback(FAB);
export default HapticFAB;
diff --git a/mobile/components/ui/HapticIconButton.js b/mobile/components/ui/HapticIconButton.js
index ea44b7b..ed0ac2b 100644
--- a/mobile/components/ui/HapticIconButton.js
+++ b/mobile/components/ui/HapticIconButton.js
@@ -1,14 +1,6 @@
-import React from 'react';
import { IconButton } from 'react-native-paper';
-import * as Haptics from 'expo-haptics';
+import { withHapticFeedback } from './hapticUtils';
-const HapticIconButton = ({ onPress, ...props }) => {
- const handlePress = (e) => {
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
- if (onPress) onPress(e);
- };
-
- return ;
-};
+const HapticIconButton = withHapticFeedback(IconButton);
export default HapticIconButton;
diff --git a/mobile/components/ui/HapticList.js b/mobile/components/ui/HapticList.js
index 8195204..b77073b 100644
--- a/mobile/components/ui/HapticList.js
+++ b/mobile/components/ui/HapticList.js
@@ -1,24 +1,7 @@
-import React from 'react';
import { List } from 'react-native-paper';
-import * as Haptics from 'expo-haptics';
+import { withHapticFeedback } from './hapticUtils';
-const HapticListItem = ({ onPress, ...props }) => {
- const handlePress = (e) => {
- if (onPress) {
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
- onPress(e);
- }
- };
- return ;
-};
-
-const HapticListAccordion = ({ onPress, ...props }) => {
- const handlePress = (e) => {
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
- if (onPress) onPress(e);
- };
-
- return ;
-};
+const HapticListItem = withHapticFeedback(List.Item, { onlyWhenHandler: true });
+const HapticListAccordion = withHapticFeedback(List.Accordion, { onlyWhenHandler: true });
export { HapticListItem, HapticListAccordion };
diff --git a/mobile/components/ui/HapticMenu.js b/mobile/components/ui/HapticMenu.js
index de6cd70..d6aedb1 100644
--- a/mobile/components/ui/HapticMenu.js
+++ b/mobile/components/ui/HapticMenu.js
@@ -1,14 +1,8 @@
import React from 'react';
import { Menu } from 'react-native-paper';
-import * as Haptics from 'expo-haptics';
+import { withHapticFeedback } from './hapticUtils';
-const HapticMenuItem = ({ onPress, ...props }) => {
- const handlePress = (e) => {
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
- if (onPress) onPress(e);
- };
- return ;
-};
+const HapticMenuItem = withHapticFeedback(Menu.Item);
const HapticMenu = ({ children, ...props }) => {
return ;
diff --git a/mobile/components/ui/HapticSegmentedButtons.js b/mobile/components/ui/HapticSegmentedButtons.js
index 28914b9..74542b7 100644
--- a/mobile/components/ui/HapticSegmentedButtons.js
+++ b/mobile/components/ui/HapticSegmentedButtons.js
@@ -1,13 +1,8 @@
-import React from 'react';
import { SegmentedButtons } from 'react-native-paper';
-import * as Haptics from 'expo-haptics';
+import { withHapticFeedback } from './hapticUtils';
-const HapticSegmentedButtons = ({ onValueChange, ...props }) => {
- const handleChange = (value) => {
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
- if (onValueChange) onValueChange(value);
- };
- return ;
-};
+const HapticSegmentedButtons = withHapticFeedback(SegmentedButtons, {
+ pressProp: 'onValueChange',
+});
export default HapticSegmentedButtons;
diff --git a/mobile/components/ui/hapticUtils.js b/mobile/components/ui/hapticUtils.js
new file mode 100644
index 0000000..bde0a46
--- /dev/null
+++ b/mobile/components/ui/hapticUtils.js
@@ -0,0 +1,56 @@
+import React, { forwardRef, useCallback } from 'react';
+import * as Haptics from 'expo-haptics';
+
+/**
+ * Higher-Order Component to add haptic feedback to pressable components.
+ *
+ * @param {React.Component} WrappedComponent - The component to wrap.
+ * @param {Object} options - Configuration options.
+ * @param {string} options.pressProp - The name of the prop that handles the press event (default: 'onPress').
+ * @param {boolean} options.onlyWhenHandler - If true, haptics only trigger if the handler prop is provided.
+ * @returns {React.Component} - The wrapped component with haptic feedback.
+ */
+export const withHapticFeedback = (WrappedComponent, options = {}) => {
+ const { pressProp = 'onPress', onlyWhenHandler = false } = options;
+
+ const WithHaptic = forwardRef((props, ref) => {
+ const originalHandler = props[pressProp];
+
+ const handlePress = useCallback(
+ (...args) => {
+ if (!onlyWhenHandler || originalHandler) {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ }
+ if (originalHandler) {
+ originalHandler(...args);
+ }
+ },
+ [originalHandler]
+ );
+
+ // Only pass the intercepted handler if we're not in "onlyWhenHandler" mode OR if the handler exists.
+ // However, some components might expect the handler prop to always be present or undefined.
+ // If onlyWhenHandler is true and originalHandler is missing, we pass undefined to avoid attaching a no-op handler that might make the component look interactive.
+ const handlerProps = {};
+ if (onlyWhenHandler && !originalHandler) {
+ // Do not attach our handler
+ handlerProps[pressProp] = undefined;
+ } else {
+ handlerProps[pressProp] = handlePress;
+ }
+
+ return ;
+ });
+
+ const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
+ WithHaptic.displayName = `WithHaptic(${displayName})`;
+
+ return WithHaptic;
+};
+
+/**
+ * Triggers a light haptic feedback for pull-to-refresh actions.
+ */
+export const triggerPullRefreshHaptic = async () => {
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+};
diff --git a/mobile/screens/FriendsScreen.js b/mobile/screens/FriendsScreen.js
index 110fd0d..c778a95 100644
--- a/mobile/screens/FriendsScreen.js
+++ b/mobile/screens/FriendsScreen.js
@@ -11,7 +11,7 @@ import {
} from "react-native-paper";
import HapticIconButton from '../components/ui/HapticIconButton';
import { HapticListAccordion } from '../components/ui/HapticList';
-import * as Haptics from "expo-haptics";
+import { triggerPullRefreshHaptic } from '../components/ui/hapticUtils';
import { getFriendsBalance, getGroups } from "../api/groups";
import { AuthContext } from "../context/AuthContext";
import { formatCurrency } from "../utils/currency";
@@ -61,7 +61,7 @@ const FriendsScreen = () => {
const onRefresh = async () => {
setIsRefreshing(true);
- await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ await triggerPullRefreshHaptic();
await fetchData(false);
setIsRefreshing(false);
};
From 0c117e1ec9ec1c5bf7aee3303bf220a0015508aa Mon Sep 17 00:00:00 2001
From: Devasy Patel <110348311+Devasy@users.noreply.github.com>
Date: Mon, 9 Feb 2026 17:25:53 +0530
Subject: [PATCH 4/4] Update mobile/components/ui/HapticMenu.js
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---
mobile/components/ui/HapticMenu.js | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/mobile/components/ui/HapticMenu.js b/mobile/components/ui/HapticMenu.js
index d6aedb1..02ffe2a 100644
--- a/mobile/components/ui/HapticMenu.js
+++ b/mobile/components/ui/HapticMenu.js
@@ -4,9 +4,10 @@ import { withHapticFeedback } from './hapticUtils';
const HapticMenuItem = withHapticFeedback(Menu.Item);
-const HapticMenu = ({ children, ...props }) => {
- return ;
-};
+const HapticMenu = React.forwardRef(({ children, ...props }, ref) => {
+ return ;
+});
+HapticMenu.displayName = 'HapticMenu';
HapticMenu.Item = HapticMenuItem;