From ccf189c335674e65ea1232fbf4ae98118dd85a14 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 20:14:38 +0000 Subject: [PATCH 1/4] [jules] enhance: Add password strength meter to signup Co-authored-by: Devasy23 <110348311+Devasy23@users.noreply.github.com> --- .Jules/changelog.md | 9 ++ .Jules/todo.md | 4 + web/components/ui/PasswordStrength.tsx | 127 +++++++++++++++++++++++++ web/pages/Auth.tsx | 3 + 4 files changed, 143 insertions(+) create mode 100644 web/components/ui/PasswordStrength.tsx diff --git a/.Jules/changelog.md b/.Jules/changelog.md index 007d6531..15ca1bb4 100644 --- a/.Jules/changelog.md +++ b/.Jules/changelog.md @@ -7,6 +7,15 @@ ## [Unreleased] ### Added +- **Password Strength Meter:** Added a visual password strength indicator to the signup form. + - **Features:** + - Real-time strength calculation (Length, Uppercase, Lowercase, Number, Symbol). + - Visual feedback with segmented progress bar and color coding. + - Specific criteria checklist (8+ chars, Mixed case, Number, Symbol). + - Dual-theme support (Neobrutalism & Glassmorphism). + - Accessible ARIA live region for screen readers. + - **Technical:** Created `web/components/ui/PasswordStrength.tsx`. Integrated into `web/pages/Auth.tsx`. + - **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 de49cb80..04371ba6 100644 --- a/.Jules/todo.md +++ b/.Jules/todo.md @@ -163,3 +163,7 @@ - Completed: 2026-01-21 - Files modified: `mobile/screens/HomeScreen.js`, `mobile/screens/GroupDetailsScreen.js`, `mobile/screens/FriendsScreen.js` - Impact: Native feel, users can easily refresh data +- [x] **[polish]** Password strength meter for signup + - Completed: 2026-02-08 + - Files modified: `web/components/ui/PasswordStrength.tsx`, `web/pages/Auth.tsx` + - Impact: Provides visual feedback on password complexity during signup diff --git a/web/components/ui/PasswordStrength.tsx b/web/components/ui/PasswordStrength.tsx new file mode 100644 index 00000000..fab7b0dc --- /dev/null +++ b/web/components/ui/PasswordStrength.tsx @@ -0,0 +1,127 @@ +import React, { useMemo } from 'react'; +import { useTheme } from '../../contexts/ThemeContext'; +import { THEMES } from '../../constants'; +import { Check } from 'lucide-react'; + +interface PasswordStrengthProps { + password?: string; +} + +export const PasswordStrength: React.FC = ({ password = '' }) => { + const { style } = useTheme(); + + const { score, label, metCriteria } = useMemo(() => { + let s = 0; + const criteria = { + length: password.length >= 8, + hasUpper: /[A-Z]/.test(password), + hasLower: /[a-z]/.test(password), + hasNumber: /[0-9]/.test(password), + hasSpecial: /[^A-Za-z0-9]/.test(password), + }; + + let l = 'Weak'; + + if (password.length > 0) { + if (password.length < 6) { + s = 1; // Very Weak + l = 'Too Short'; + } else { + s = 1; + // Bonus for length + if (criteria.length) s++; + + // Bonus for complexity + const complexity = (criteria.hasUpper ? 1 : 0) + + (criteria.hasLower ? 1 : 0) + + (criteria.hasNumber ? 1 : 0) + + (criteria.hasSpecial ? 1 : 0); + + if (complexity >= 2) s++; + if (complexity >= 4) s++; // Max bonus + + // Adjust label based on final score + if (s >= 4) l = 'Strong'; + else if (s === 3) l = 'Good'; + else if (s === 2) l = 'Fair'; + else l = 'Weak'; + } + } else { + l = ''; + } + + // Normalize score 0-4 + if (s > 4) s = 4; + + return { score: s, label: l, metCriteria: criteria }; + }, [password]); + + const getColor = (s: number) => { + switch (s) { + case 1: return 'bg-red-500'; + case 2: return 'bg-orange-500'; + case 3: return 'bg-yellow-500'; + case 4: return 'bg-green-500'; + default: return 'bg-gray-200 dark:bg-zinc-700'; + } + }; + + const isNeo = style === THEMES.NEOBRUTALISM; + + if (!password) return null; + + return ( +
+ {/* Strength Bar */} +
+ {[1, 2, 3, 4].map((level) => ( +
= level + ? getColor(score) + : (isNeo ? 'bg-gray-200 dark:bg-zinc-800' : 'bg-white/10') + } ${isNeo ? 'border-2 border-black' : 'rounded-full'}`} + /> + ))} +
+ + {/* Label and Criteria */} +
+ + {label} + +
+ +
+
+ {metCriteria.length ? :
} + 8+ characters +
+
+ {(metCriteria.hasUpper && metCriteria.hasLower) ? :
} + Mixed case +
+
+ {metCriteria.hasNumber ? :
} + Number +
+
+ {metCriteria.hasSpecial ? :
} + Symbol +
+
+ + {/* Hidden live region for accessibility */} +
+ Password strength: {label}. + {score < 4 && label ? "Add more characters, numbers, or symbols to strengthen." : ""} +
+
+ ); +}; diff --git a/web/pages/Auth.tsx b/web/pages/Auth.tsx index 4f35a289..5f0efbee 100644 --- a/web/pages/Auth.tsx +++ b/web/pages/Auth.tsx @@ -4,6 +4,7 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Button } from '../components/ui/Button'; import { Input } from '../components/ui/Input'; +import { PasswordStrength } from '../components/ui/PasswordStrength'; import { Spinner } from '../components/ui/Spinner'; import { THEMES } from '../constants'; import { useAuth } from '../contexts/AuthContext'; @@ -299,6 +300,8 @@ export const Auth = () => { className={isNeo ? 'rounded-none' : ''} /> + {!isLogin && } + {error && ( Date: Mon, 9 Feb 2026 19:37:13 +0530 Subject: [PATCH 2/4] Update web/components/ui/PasswordStrength.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- web/components/ui/PasswordStrength.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/ui/PasswordStrength.tsx b/web/components/ui/PasswordStrength.tsx index fab7b0dc..a9e6a590 100644 --- a/web/components/ui/PasswordStrength.tsx +++ b/web/components/ui/PasswordStrength.tsx @@ -118,10 +118,10 @@ export const PasswordStrength: React.FC = ({ password = '
{/* Hidden live region for accessibility */} -
+ Password strength: {label}. {score < 4 && label ? "Add more characters, numbers, or symbols to strengthen." : ""} -
+
); }; From 23e0b2bc918116136dd51f6213abac7d43cc388c Mon Sep 17 00:00:00 2001 From: Devasy Patel <110348311+Devasy@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:37:24 +0530 Subject: [PATCH 3/4] Update web/components/ui/PasswordStrength.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- web/components/ui/PasswordStrength.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/ui/PasswordStrength.tsx b/web/components/ui/PasswordStrength.tsx index a9e6a590..be98edef 100644 --- a/web/components/ui/PasswordStrength.tsx +++ b/web/components/ui/PasswordStrength.tsx @@ -71,7 +71,7 @@ export const PasswordStrength: React.FC = ({ password = ' if (!password) return null; return ( -
+
{/* Strength Bar */}
{[1, 2, 3, 4].map((level) => ( From f43533e6d8a5e832dae1aaf1b23347d11ab5b5f1 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 14:18:57 +0000 Subject: [PATCH 4/4] [jules] fix: Update password strength criteria to 6 chars and merge main Co-authored-by: Devasy23 <110348311+Devasy23@users.noreply.github.com> --- .Jules/changelog.md | 9 ++- .Jules/todo.md | 11 ++-- mobile/components/ui/HapticAppbar.js | 7 +++ mobile/components/ui/HapticButton.js | 6 ++ mobile/components/ui/HapticCard.js | 12 ++++ mobile/components/ui/HapticCheckbox.js | 6 ++ mobile/components/ui/HapticFAB.js | 6 ++ mobile/components/ui/HapticIconButton.js | 6 ++ mobile/components/ui/HapticList.js | 7 +++ mobile/components/ui/HapticMenu.js | 14 +++++ .../components/ui/HapticSegmentedButtons.js | 8 +++ mobile/components/ui/hapticUtils.js | 56 +++++++++++++++++++ mobile/screens/AccountScreen.js | 11 ++-- mobile/screens/AddExpenseScreen.js | 26 ++++----- mobile/screens/EditProfileScreen.js | 14 +++-- mobile/screens/FriendsScreen.js | 13 +++-- 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 +-- web/components/ui/PasswordStrength.tsx | 4 +- 24 files changed, 243 insertions(+), 97 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 create mode 100644 mobile/components/ui/hapticUtils.js diff --git a/.Jules/changelog.md b/.Jules/changelog.md index 15ca1bb4..11fc864e 100644 --- a/.Jules/changelog.md +++ b/.Jules/changelog.md @@ -11,11 +11,18 @@ - **Features:** - Real-time strength calculation (Length, Uppercase, Lowercase, Number, Symbol). - Visual feedback with segmented progress bar and color coding. - - Specific criteria checklist (8+ chars, Mixed case, Number, Symbol). + - Specific criteria checklist (6+ chars, Mixed case, Number, Symbol). - Dual-theme support (Neobrutalism & Glassmorphism). - Accessible ARIA live region for screen readers. - **Technical:** Created `web/components/ui/PasswordStrength.tsx`. Integrated into `web/pages/Auth.tsx`. +- **Mobile Haptics:** Implemented system-wide haptic feedback for all interactive elements. + - **Features:** + - 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. + - **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 04371ba6..ebb0c7a5 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 00000000..b51913b1 --- /dev/null +++ b/mobile/components/ui/HapticAppbar.js @@ -0,0 +1,7 @@ +import { Appbar } from 'react-native-paper'; +import { withHapticFeedback } from './hapticUtils'; + +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 new file mode 100644 index 00000000..f8b821a3 --- /dev/null +++ b/mobile/components/ui/HapticButton.js @@ -0,0 +1,6 @@ +import { Button } from 'react-native-paper'; +import { withHapticFeedback } from './hapticUtils'; + +const HapticButton = withHapticFeedback(Button); + +export default HapticButton; diff --git a/mobile/components/ui/HapticCard.js b/mobile/components/ui/HapticCard.js new file mode 100644 index 00000000..1d585810 --- /dev/null +++ b/mobile/components/ui/HapticCard.js @@ -0,0 +1,12 @@ +import { Card } from 'react-native-paper'; +import { withHapticFeedback } from './hapticUtils'; + +const HapticCard = withHapticFeedback(Card, { onlyWhenHandler: true }); + +// 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 00000000..bdb2b87f --- /dev/null +++ b/mobile/components/ui/HapticCheckbox.js @@ -0,0 +1,6 @@ +import { Checkbox } from 'react-native-paper'; +import { withHapticFeedback } from './hapticUtils'; + +const HapticCheckboxItem = withHapticFeedback(Checkbox.Item); + +export default HapticCheckboxItem; diff --git a/mobile/components/ui/HapticFAB.js b/mobile/components/ui/HapticFAB.js new file mode 100644 index 00000000..d4d8f49f --- /dev/null +++ b/mobile/components/ui/HapticFAB.js @@ -0,0 +1,6 @@ +import { FAB } from 'react-native-paper'; +import { withHapticFeedback } from './hapticUtils'; + +const HapticFAB = withHapticFeedback(FAB); + +export default HapticFAB; diff --git a/mobile/components/ui/HapticIconButton.js b/mobile/components/ui/HapticIconButton.js new file mode 100644 index 00000000..ed0ac2b7 --- /dev/null +++ b/mobile/components/ui/HapticIconButton.js @@ -0,0 +1,6 @@ +import { IconButton } from 'react-native-paper'; +import { withHapticFeedback } from './hapticUtils'; + +const HapticIconButton = withHapticFeedback(IconButton); + +export default HapticIconButton; diff --git a/mobile/components/ui/HapticList.js b/mobile/components/ui/HapticList.js new file mode 100644 index 00000000..b77073b8 --- /dev/null +++ b/mobile/components/ui/HapticList.js @@ -0,0 +1,7 @@ +import { List } from 'react-native-paper'; +import { withHapticFeedback } from './hapticUtils'; + +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 new file mode 100644 index 00000000..02ffe2af --- /dev/null +++ b/mobile/components/ui/HapticMenu.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { Menu } from 'react-native-paper'; +import { withHapticFeedback } from './hapticUtils'; + +const HapticMenuItem = withHapticFeedback(Menu.Item); + +const HapticMenu = React.forwardRef(({ children, ...props }, ref) => { + return {children}; +}); +HapticMenu.displayName = 'HapticMenu'; + +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 00000000..74542b73 --- /dev/null +++ b/mobile/components/ui/HapticSegmentedButtons.js @@ -0,0 +1,8 @@ +import { SegmentedButtons } from 'react-native-paper'; +import { withHapticFeedback } from './hapticUtils'; + +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 00000000..bde0a46f --- /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/AccountScreen.js b/mobile/screens/AccountScreen.js index c4ed8698..ac130a6d 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 f5f58a3d..958873f4 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" /> - setMenuVisible(false)} anchor={ - + } > {members.map((member) => ( - { setPayerId(member.userId); @@ -394,10 +394,10 @@ const AddExpenseScreen = ({ route, navigation }) => { title={member.user.name} /> ))} - + Split Method - { {renderSplitInputs()} - + ); diff --git a/mobile/screens/EditProfileScreen.js b/mobile/screens/EditProfileScreen.js index 33a8faf1..990b8aef 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 ad9bea24..c778a95c 100644 --- a/mobile/screens/FriendsScreen.js +++ b/mobile/screens/FriendsScreen.js @@ -5,12 +5,13 @@ import { Appbar, Avatar, Divider, - IconButton, List, Text, useTheme, } from "react-native-paper"; -import * as Haptics from "expo-haptics"; +import HapticIconButton from '../components/ui/HapticIconButton'; +import { HapticListAccordion } from '../components/ui/HapticList'; +import { triggerPullRefreshHaptic } from '../components/ui/hapticUtils'; import { getFriendsBalance, getGroups } from "../api/groups"; import { AuthContext } from "../context/AuthContext"; import { formatCurrency } from "../utils/currency"; @@ -60,7 +61,7 @@ const FriendsScreen = () => { const onRefresh = async () => { setIsRefreshing(true); - await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + await triggerPullRefreshHaptic(); await fetchData(false); setIsRefreshing(false); }; @@ -94,7 +95,7 @@ const FriendsScreen = () => { } return ( - { /> ); })} - + ); }; @@ -238,7 +239,7 @@ const FriendsScreen = () => { 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 bcb0018e..7ac1ee8c 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 f45c099a..6dd61118 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 373bb0ab..d2f3c383 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 a1fc05b0..122769d6 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 aa5e94a1..194db5bb 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 c3ba18f5..d40f3627 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 8abf5cd6..d6a65c1b 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 diff --git a/web/components/ui/PasswordStrength.tsx b/web/components/ui/PasswordStrength.tsx index be98edef..605fd618 100644 --- a/web/components/ui/PasswordStrength.tsx +++ b/web/components/ui/PasswordStrength.tsx @@ -13,7 +13,7 @@ export const PasswordStrength: React.FC = ({ password = ' const { score, label, metCriteria } = useMemo(() => { let s = 0; const criteria = { - length: password.length >= 8, + length: password.length >= 6, hasUpper: /[A-Z]/.test(password), hasLower: /[a-z]/.test(password), hasNumber: /[0-9]/.test(password), @@ -101,7 +101,7 @@ export const PasswordStrength: React.FC = ({ password = '
{metCriteria.length ? :
} - 8+ characters + 6+ characters
{(metCriteria.hasUpper && metCriteria.hasLower) ? :
}