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/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" 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;