From ddbf900828f049fca345a6f7ff141fe22933ebcb Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sun, 5 Apr 2026 20:11:29 +0000
Subject: [PATCH 1/2] [jules] ux: Implement swipe-to-delete for expenses with
undo option
Implemented swipe-to-delete functionality for expenses on the Group Details screen in the mobile app.
- Created an optimistic deletion flow with a 5-second undo Snackbar using `react-native-paper`.
- Wrapped expense cards with `Swipeable` from `react-native-gesture-handler`.
- Added the `deleteExpense` function to the API client (`mobile/api/groups.js`).
- Created `mobile/babel.config.js` to support the required `react-native-reanimated` plugin.
- Added necessary ref-based timeout management to prevent state mismatch during rapid interactions.
- Updated `.Jules/todo.md` and `.Jules/changelog.md` to reflect the completed task.
Co-authored-by: Devasy23 <110348311+Devasy23@users.noreply.github.com>
---
.Jules/changelog.md | 9 ++
.Jules/todo.md | 9 +-
mobile/api/groups.js | 3 +
mobile/babel.config.js | 7 ++
mobile/screens/GroupDetailsScreen.js | 138 +++++++++++++++++++++++----
5 files changed, 145 insertions(+), 21 deletions(-)
create mode 100644 mobile/babel.config.js
diff --git a/.Jules/changelog.md b/.Jules/changelog.md
index 11fc864e..00ed219f 100644
--- a/.Jules/changelog.md
+++ b/.Jules/changelog.md
@@ -31,6 +31,15 @@
- Covered Auth, Dashboard, Groups, and Utility screens.
- **Technical:** Updated all files in `mobile/screens/` to compliant with React Native accessibility standards.
+- **Mobile Swipe-to-Delete Expenses:** Implemented swipe-to-delete functionality for expenses in Group Details.
+ - **Features:**
+ - Right-to-left swipe reveals a Delete action on user-paid expenses.
+ - Uses `react-native-gesture-handler/Swipeable` and `react-native-reanimated`.
+ - Optimistic UI updates to hide the expense immediately.
+ - Snackbar notification with "Undo" button (5-second timeout) before finalizing the API call.
+ - Implemented a ref-based timeout dictionary to prevent state mismatch during rapid deletion and undo operations.
+ - **Technical:** Modified `mobile/screens/GroupDetailsScreen.js` and added `deleteExpense` to `mobile/api/groups.js`. Modified `mobile/babel.config.js` to support Reanimated plugin.
+
- **Mobile Pull-to-Refresh:** Implemented native pull-to-refresh interactions with haptic feedback for key lists.
- **Features:**
- Integrated `RefreshControl` into `HomeScreen`, `FriendsScreen`, and `GroupDetailsScreen`.
diff --git a/.Jules/todo.md b/.Jules/todo.md
index ebb0c7a5..513f6649 100644
--- a/.Jules/todo.md
+++ b/.Jules/todo.md
@@ -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
- - Impact: Quick expense management
- - Size: ~55 lines
+ - Context: Added swipeable rows with optimistic delete and undo Snackbar action using `react-native-gesture-handler`.
+ - Impact: Quick expense management and forgiving UX on mobile.
+ - Size: ~100 lines
- Added: 2026-01-01
- [x] **[style]** Haptic feedback on all button presses
diff --git a/mobile/api/groups.js b/mobile/api/groups.js
index 8cf9cdba..c14af702 100644
--- a/mobile/api/groups.js
+++ b/mobile/api/groups.js
@@ -18,6 +18,9 @@ export const getGroupMembers = (groupId) =>
export const getGroupExpenses = (groupId) =>
apiClient.get(`/groups/${groupId}/expenses`);
+export const deleteExpense = (groupId, expenseId) =>
+ apiClient.delete(`/groups/${groupId}/expenses/${expenseId}`);
+
export const createGroup = (name) => apiClient.post("/groups", { name });
export const joinGroup = (joinCode) =>
diff --git a/mobile/babel.config.js b/mobile/babel.config.js
new file mode 100644
index 00000000..db538eba
--- /dev/null
+++ b/mobile/babel.config.js
@@ -0,0 +1,7 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ plugins: ['react-native-reanimated/plugin'],
+ };
+};
diff --git a/mobile/screens/GroupDetailsScreen.js b/mobile/screens/GroupDetailsScreen.js
index 7ac1ee8c..b78b42b8 100644
--- a/mobile/screens/GroupDetailsScreen.js
+++ b/mobile/screens/GroupDetailsScreen.js
@@ -1,11 +1,13 @@
-import { useContext, useEffect, useState } from "react";
-import { Alert, FlatList, RefreshControl, StyleSheet, Text, View } from "react-native";
+import { useContext, useEffect, useState, useRef } from "react";
+import { Alert, FlatList, RefreshControl, StyleSheet, Text, View, Animated } from "react-native";
import {
ActivityIndicator,
Paragraph,
Title,
useTheme,
+ Snackbar,
} from "react-native-paper";
+import { Swipeable } from "react-native-gesture-handler";
import HapticCard from '../components/ui/HapticCard';
import HapticFAB from '../components/ui/HapticFAB';
import HapticIconButton from '../components/ui/HapticIconButton';
@@ -14,6 +16,7 @@ import {
getGroupExpenses,
getGroupMembers,
getOptimizedSettlements,
+ deleteExpense,
} from "../api/groups";
import { AuthContext } from "../context/AuthContext";
@@ -26,6 +29,10 @@ const GroupDetailsScreen = ({ route, navigation }) => {
const [settlements, setSettlements] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
+ const [hiddenExpenses, setHiddenExpenses] = useState([]);
+ const [snackbarVisible, setSnackbarVisible] = useState(false);
+ const [snackbarMessage, setSnackbarMessage] = useState("");
+ const deleteTimeouts = useRef({});
// Currency configuration - can be made configurable later
const currency = "₹"; // Default to INR, can be changed to '$' for USD
@@ -102,23 +109,81 @@ const GroupDetailsScreen = ({ route, navigation }) => {
balanceText = "You are settled for this expense.";
}
+ if (hiddenExpenses.includes(item._id)) {
+ return null;
+ }
+
+ const handleDeleteSwipe = () => {
+ // Optimistic delete
+ setHiddenExpenses((prev) => [...prev, item._id]);
+ setSnackbarMessage(`Deleted "${item.description}"`);
+ setSnackbarVisible(true);
+
+ const timeoutId = setTimeout(async () => {
+ try {
+ await deleteExpense(groupId, item._id);
+ setExpenses((prev) => prev.filter((exp) => exp._id !== item._id));
+ } catch (error) {
+ console.error("Failed to delete expense:", error);
+ setHiddenExpenses((prev) => prev.filter((id) => id !== item._id));
+ Alert.alert("Error", "Failed to delete expense.");
+ }
+ setHiddenExpenses((prev) => prev.filter((id) => id !== item._id));
+ delete deleteTimeouts.current[item._id];
+ }, 5000); // 5 seconds to undo
+
+ deleteTimeouts.current[item._id] = { timeoutId, item };
+ };
+
+ const renderRightActions = (progress, dragX) => {
+ const trans = dragX.interpolate({
+ inputRange: [-100, 0],
+ outputRange: [1, 0],
+ extrapolate: 'clamp',
+ });
+ return (
+
+
+ Delete
+
+
+ );
+ };
+
return (
- {
+ if (paidByMe) {
+ handleDeleteSwipe();
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
+ }
+ }}
>
-
- {item.description}
- Amount: {formatCurrency(item.amount)}
-
- Paid by: {getMemberName(item.paidBy || item.createdBy)}
-
- {balanceText}
-
-
+
+
+ {item.description}
+ Amount: {formatCurrency(item.amount)}
+
+ Paid by: {getMemberName(item.paidBy || item.createdBy)}
+
+ {balanceText}
+
+
+
);
};
@@ -209,6 +274,19 @@ const GroupDetailsScreen = ({ route, navigation }) => {
>
);
+ const handleUndoGlobal = () => {
+ Object.keys(deleteTimeouts.current).forEach(id => {
+ clearTimeout(deleteTimeouts.current[id].timeoutId);
+ setHiddenExpenses((prev) => prev.filter((expId) => expId !== id));
+ });
+ deleteTimeouts.current = {};
+ setSnackbarVisible(false);
+ };
+
+ const handleSnackbarDismiss = () => {
+ setSnackbarVisible(false);
+ };
+
return (
{
accessibilityLabel="Add expense"
accessibilityRole="button"
/>
+
+
+ {snackbarMessage}
+
);
};
@@ -338,6 +428,20 @@ const styles = StyleSheet.create({
color: "#666",
paddingVertical: 8,
},
+ deleteAction: {
+ backgroundColor: '#d32f2f',
+ justifyContent: 'center',
+ alignItems: 'flex-end',
+ marginBottom: 16,
+ borderRadius: 8,
+ flex: 1,
+ paddingRight: 20,
+ },
+ deleteActionText: {
+ color: 'white',
+ fontWeight: 'bold',
+ fontSize: 16,
+ },
});
export default GroupDetailsScreen;
From 9198ac097f3c4c15323293e206b4040ec33c3ae6 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sun, 5 Apr 2026 20:17:45 +0000
Subject: [PATCH 2/2] [jules] ux: Implement swipe-to-delete for expenses with
undo option
Implemented swipe-to-delete functionality for expenses on the Group Details screen in the mobile app.
- Created an optimistic deletion flow with a 5-second undo Snackbar using `react-native-paper`.
- Wrapped expense cards with `Swipeable` from `react-native-gesture-handler`.
- Added the `deleteExpense` function to the API client (`mobile/api/groups.js`).
- Created `mobile/babel.config.js` to support the required `react-native-reanimated` plugin.
- Added necessary ref-based timeout management to prevent state mismatch during rapid interactions.
- Updated `.Jules/todo.md` and `.Jules/changelog.md` to reflect the completed task.
Co-authored-by: Devasy23 <110348311+Devasy23@users.noreply.github.com>
---
mobile/package-lock.json | 352 ++++++++++++++++++++++++++++++++++++++-
mobile/package.json | 2 +
2 files changed, 351 insertions(+), 3 deletions(-)
diff --git a/mobile/package-lock.json b/mobile/package-lock.json
index c3452165..c2521f10 100644
--- a/mobile/package-lock.json
+++ b/mobile/package-lock.json
@@ -21,7 +21,9 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
+ "react-native-gesture-handler": "^2.31.0",
"react-native-paper": "^5.14.5",
+ "react-native-reanimated": "^4.3.0",
"react-native-safe-area-context": "^5.4.0",
"react-native-screens": "^4.11.1",
"react-native-web": "^0.21.0"
@@ -1372,6 +1374,22 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/plugin-transform-template-literals": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz",
+ "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/plugin-transform-typescript": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz",
@@ -1541,6 +1559,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/@egjs/hammerjs": {
+ "version": "2.0.17",
+ "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
+ "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hammerjs": "^2.0.36"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/@expo/code-signing-certificates": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.5.tgz",
@@ -2911,6 +2941,162 @@
"node": ">= 20.19.4"
}
},
+ "node_modules/@react-native/metro-babel-transformer": {
+ "version": "0.84.1",
+ "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.84.1.tgz",
+ "integrity": "sha512-NswINguTz0eg1Dc0oGO/1dejXSr6iQaz8/NnCRn5HJdA3dGfqadS7zlYv0YjiWpgKgcW6uENaIEgJOQww0KSpw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/core": "^7.25.2",
+ "@react-native/babel-preset": "0.84.1",
+ "hermes-parser": "0.32.0",
+ "nullthrows": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 20.19.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "*"
+ }
+ },
+ "node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/babel-plugin-codegen": {
+ "version": "0.84.1",
+ "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.84.1.tgz",
+ "integrity": "sha512-vorvcvptGxtK0qTDCFQb+W3CU6oIhzcX5dduetWRBoAhXdthEQM0MQnF+GTXoXL8/luffKgy7PlZRG/WeI/oRQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/traverse": "^7.25.3",
+ "@react-native/codegen": "0.84.1"
+ },
+ "engines": {
+ "node": ">= 20.19.4"
+ }
+ },
+ "node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/babel-preset": {
+ "version": "0.84.1",
+ "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.84.1.tgz",
+ "integrity": "sha512-3GpmCKk21f4oe32bKIdmkdn+WydvhhZL+1nsoFBGi30Qrq9vL16giKu31OcnWshYz139x+mVAvCyoyzgn8RXSw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/core": "^7.25.2",
+ "@babel/plugin-proposal-export-default-from": "^7.24.7",
+ "@babel/plugin-syntax-dynamic-import": "^7.8.3",
+ "@babel/plugin-syntax-export-default-from": "^7.24.7",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+ "@babel/plugin-transform-async-generator-functions": "^7.25.4",
+ "@babel/plugin-transform-async-to-generator": "^7.24.7",
+ "@babel/plugin-transform-block-scoping": "^7.25.0",
+ "@babel/plugin-transform-class-properties": "^7.25.4",
+ "@babel/plugin-transform-classes": "^7.25.4",
+ "@babel/plugin-transform-destructuring": "^7.24.8",
+ "@babel/plugin-transform-flow-strip-types": "^7.25.2",
+ "@babel/plugin-transform-for-of": "^7.24.7",
+ "@babel/plugin-transform-modules-commonjs": "^7.24.8",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7",
+ "@babel/plugin-transform-optional-catch-binding": "^7.24.7",
+ "@babel/plugin-transform-optional-chaining": "^7.24.8",
+ "@babel/plugin-transform-private-methods": "^7.24.7",
+ "@babel/plugin-transform-private-property-in-object": "^7.24.7",
+ "@babel/plugin-transform-react-display-name": "^7.24.7",
+ "@babel/plugin-transform-react-jsx": "^7.25.2",
+ "@babel/plugin-transform-react-jsx-self": "^7.24.7",
+ "@babel/plugin-transform-react-jsx-source": "^7.24.7",
+ "@babel/plugin-transform-regenerator": "^7.24.7",
+ "@babel/plugin-transform-runtime": "^7.24.7",
+ "@babel/plugin-transform-typescript": "^7.25.2",
+ "@babel/plugin-transform-unicode-regex": "^7.24.7",
+ "@react-native/babel-plugin-codegen": "0.84.1",
+ "babel-plugin-syntax-hermes-parser": "0.32.0",
+ "babel-plugin-transform-flow-enums": "^0.0.2",
+ "react-refresh": "^0.14.0"
+ },
+ "engines": {
+ "node": ">= 20.19.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "*"
+ }
+ },
+ "node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/codegen": {
+ "version": "0.84.1",
+ "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.84.1.tgz",
+ "integrity": "sha512-n1RIU0QAavgCg1uC5+s53arL7/mpM+16IBhJ3nCFSd/iK5tUmCwxQDcIDC703fuXfpub/ZygeSjVN8bcOWn0gA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/core": "^7.25.2",
+ "@babel/parser": "^7.25.3",
+ "hermes-parser": "0.32.0",
+ "invariant": "^2.2.4",
+ "nullthrows": "^1.1.1",
+ "tinyglobby": "^0.2.15",
+ "yargs": "^17.6.2"
+ },
+ "engines": {
+ "node": ">= 20.19.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "*"
+ }
+ },
+ "node_modules/@react-native/metro-babel-transformer/node_modules/babel-plugin-syntax-hermes-parser": {
+ "version": "0.32.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.0.tgz",
+ "integrity": "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "hermes-parser": "0.32.0"
+ }
+ },
+ "node_modules/@react-native/metro-babel-transformer/node_modules/hermes-estree": {
+ "version": "0.32.0",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz",
+ "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@react-native/metro-babel-transformer/node_modules/hermes-parser": {
+ "version": "0.32.0",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz",
+ "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "hermes-estree": "0.32.0"
+ }
+ },
+ "node_modules/@react-native/metro-config": {
+ "version": "0.84.1",
+ "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.84.1.tgz",
+ "integrity": "sha512-KlRawK4aXxRLlR3HYVfZKhfQp7sejQefQ/LttUWUkErhKO0AFt+yznoSLq7xwIrH9K3A3YwImHuFVtUtuDmurA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@react-native/js-polyfills": "0.84.1",
+ "@react-native/metro-babel-transformer": "0.84.1",
+ "metro-config": "^0.83.3",
+ "metro-runtime": "^0.83.3"
+ },
+ "engines": {
+ "node": ">= 20.19.4"
+ }
+ },
+ "node_modules/@react-native/metro-config/node_modules/@react-native/js-polyfills": {
+ "version": "0.84.1",
+ "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.84.1.tgz",
+ "integrity": "sha512-UsTe2AbUugsfyI7XIHMQq4E7xeC8a6GrYwuK+NohMMMJMxmyM3JkzIk+GB9e2il6ScEQNMJNaj+q+i5za8itxQ==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">= 20.19.4"
+ }
+ },
"node_modules/@react-native/normalize-colors": {
"version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.5.tgz",
@@ -3099,6 +3285,12 @@
"@types/node": "*"
}
},
+ "node_modules/@types/hammerjs": {
+ "version": "2.0.46",
+ "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz",
+ "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==",
+ "license": "MIT"
+ },
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
@@ -3132,6 +3324,24 @@
"undici-types": "~7.8.0"
}
},
+ "node_modules/@types/react": {
+ "version": "19.2.14",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-test-renderer": {
+ "version": "19.1.0",
+ "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz",
+ "integrity": "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/stack-utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
@@ -4174,6 +4384,12 @@
"hyphenate-style-name": "^1.0.3"
}
},
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@@ -4848,6 +5064,24 @@
"asap": "~2.0.3"
}
},
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -7439,10 +7673,26 @@
}
}
},
+ "node_modules/react-native-gesture-handler": {
+ "version": "2.31.0",
+ "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.31.0.tgz",
+ "integrity": "sha512-wmMs24UaRCIvIU/Nm7cSIV0FEWFsMrNBXxtpo5+9i8iCeOFFuyi8v8Dgfo7JKspcqPMUin0DBzi9DiG3vsDq5A==",
+ "license": "MIT",
+ "dependencies": {
+ "@egjs/hammerjs": "^2.0.17",
+ "@types/react-test-renderer": "^19.1.0",
+ "hoist-non-react-statics": "^3.3.0",
+ "invariant": "^2.2.4"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/react-native-is-edge-to-edge": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz",
- "integrity": "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.3.1.tgz",
+ "integrity": "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA==",
"license": "MIT",
"peerDependencies": {
"react": "*",
@@ -7494,6 +7744,33 @@
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"license": "MIT"
},
+ "node_modules/react-native-reanimated": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.3.0.tgz",
+ "integrity": "sha512-HOTTPdKtddXTOsmQxDASXEwLS3lqEHrKERD3XOgzSqWJ7L3x81Pnx7mTcKx1FKdkgomMug/XSmm1C6Z7GIowxA==",
+ "license": "MIT",
+ "dependencies": {
+ "react-native-is-edge-to-edge": "^1.3.1",
+ "semver": "^7.7.3"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "0.81 - 0.85",
+ "react-native-worklets": "0.8.x"
+ }
+ },
+ "node_modules/react-native-reanimated/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/react-native-safe-area-context": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
@@ -7549,6 +7826,45 @@
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
},
+ "node_modules/react-native-worklets": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.8.1.tgz",
+ "integrity": "sha512-oWP/lStsAHU6oYCaWDXrda/wOHVdhusQJz1e6x9gPnXdFf4ndNDAOtWCmk2zGrAnlapfyA3rM6PCQq94mPg9cw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/plugin-transform-arrow-functions": "^7.27.1",
+ "@babel/plugin-transform-class-properties": "^7.27.1",
+ "@babel/plugin-transform-classes": "^7.28.4",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
+ "@babel/plugin-transform-optional-chaining": "^7.27.1",
+ "@babel/plugin-transform-shorthand-properties": "^7.27.1",
+ "@babel/plugin-transform-template-literals": "^7.27.1",
+ "@babel/plugin-transform-unicode-regex": "^7.27.1",
+ "@babel/preset-typescript": "^7.27.1",
+ "convert-source-map": "^2.0.0",
+ "semver": "^7.7.3"
+ },
+ "peerDependencies": {
+ "@babel/core": "*",
+ "@react-native/metro-config": "*",
+ "react": "*",
+ "react-native": "0.81 - 0.85"
+ }
+ },
+ "node_modules/react-native-worklets/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "license": "ISC",
+ "peer": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/react-native/node_modules/@react-native/virtualized-lists": {
"version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz",
@@ -8659,6 +8975,36 @@
"integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==",
"license": "MIT"
},
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
diff --git a/mobile/package.json b/mobile/package.json
index a425a9c1..b9280900 100644
--- a/mobile/package.json
+++ b/mobile/package.json
@@ -22,7 +22,9 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
+ "react-native-gesture-handler": "^2.31.0",
"react-native-paper": "^5.14.5",
+ "react-native-reanimated": "^4.3.0",
"react-native-safe-area-context": "^5.4.0",
"react-native-screens": "^4.11.1",
"react-native-web": "^0.21.0"