diff --git a/.Jules/changelog.md b/.Jules/changelog.md index 11fc864e..05e9ecae 100644 --- a/.Jules/changelog.md +++ b/.Jules/changelog.md @@ -7,6 +7,13 @@ ## [Unreleased] ### Added +- **Mobile Toast Notifications:** Added a global Toast context for non-blocking feedback messages. + - **Features:** + - Uses React Native Paper `Snackbar` component. + - Replaces intrusive `Alert.alert` calls. + - Themed according to message type ('success', 'error', 'info'). + - **Technical:** Created `mobile/context/ToastContext.js` and wrapped App. Integrated into Auth and Home screens. + - **Password Strength Meter:** Added a visual password strength indicator to the signup form. - **Features:** - Real-time strength calculation (Length, Uppercase, Lowercase, Number, Symbol). diff --git a/.Jules/todo.md b/.Jules/todo.md index ebb0c7a5..123e184f 100644 --- a/.Jules/todo.md +++ b/.Jules/todo.md @@ -143,6 +143,11 @@ ## ✅ Completed Tasks +- [x] **[ux]** Global Toast Notification System for Mobile + - Completed: 2026-04-15 + - Files modified: `mobile/context/ToastContext.js`, `mobile/App.js`, `mobile/screens/LoginScreen.js`, `mobile/screens/SignupScreen.js`, `mobile/screens/HomeScreen.js` + - Impact: Replaces intrusive Alert.alert with non-blocking modern snackbar notifications for form validation and success/error messages. + - [x] **[ux]** Comprehensive empty states with illustrations - Completed: 2026-01-01 - Files modified: `web/components/ui/EmptyState.tsx`, `web/pages/Groups.tsx`, `web/pages/Friends.tsx` diff --git a/mobile/App.js b/mobile/App.js index f5496adf..acbb8612 100644 --- a/mobile/App.js +++ b/mobile/App.js @@ -2,12 +2,15 @@ import React from 'react'; import AppNavigator from './navigation/AppNavigator'; import { PaperProvider } from 'react-native-paper'; import { AuthProvider } from './context/AuthContext'; +import { ToastProvider } from './context/ToastContext'; export default function App() { return ( - + + + ); diff --git a/mobile/context/ToastContext.js b/mobile/context/ToastContext.js new file mode 100644 index 00000000..38936123 --- /dev/null +++ b/mobile/context/ToastContext.js @@ -0,0 +1,73 @@ +import React, { createContext, useState, useContext, useCallback } from 'react'; +import { StyleSheet } from 'react-native'; +import { Snackbar, useTheme } from 'react-native-paper'; + +export const ToastContext = createContext(); + +export const ToastProvider = ({ children }) => { + const [visible, setVisible] = useState(false); + const [message, setMessage] = useState(''); + const [type, setType] = useState('info'); // 'info', 'success', 'error' + const theme = useTheme(); + + const showToast = useCallback((msg, toastType = 'info') => { + setMessage(msg); + setType(toastType); + setVisible(true); + }, []); + + const hideToast = useCallback(() => { + setVisible(false); + }, []); + + const getBackgroundColor = () => { + switch (type) { + case 'success': + return '#4CAF50'; + case 'error': + return '#F44336'; + case 'info': + default: + return theme.colors.elevation.level3; + } + }; + + const getTextColor = () => { + switch (type) { + case 'success': + case 'error': + return '#FFFFFF'; + case 'info': + default: + return theme.colors.onSurface; + } + }; + + return ( + + {children} + + {message} + + + ); +}; + +export const useToast = () => useContext(ToastContext); + +const styles = StyleSheet.create({ + snackbar: { + marginBottom: 80, // Avoid bottom nav bar + }, +}); diff --git a/mobile/screens/HomeScreen.js b/mobile/screens/HomeScreen.js index d2f3c383..32c4bbbb 100644 --- a/mobile/screens/HomeScreen.js +++ b/mobile/screens/HomeScreen.js @@ -1,5 +1,5 @@ import { useContext, useEffect, useState } from "react"; -import { Alert, FlatList, RefreshControl, StyleSheet, View } from "react-native"; +import { FlatList, RefreshControl, StyleSheet, View } from "react-native"; import { ActivityIndicator, Appbar, @@ -17,10 +17,12 @@ import * as Haptics from "expo-haptics"; import { createGroup, getGroups, getOptimizedSettlements } from "../api/groups"; import { AuthContext } from "../context/AuthContext"; import { formatCurrency, getCurrencySymbol } from "../utils/currency"; +import { useToast } from "../context/ToastContext"; const HomeScreen = ({ navigation }) => { const { token, logout, user } = useContext(AuthContext); const theme = useTheme(); + const { showToast } = useToast(); const [groups, setGroups] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); @@ -94,7 +96,7 @@ const HomeScreen = ({ navigation }) => { } } catch (error) { console.error("Failed to fetch groups:", error); - Alert.alert("Error", "Failed to fetch groups."); + showToast("Failed to fetch groups.", "error"); } finally { if (showLoading) setIsLoading(false); } @@ -115,7 +117,7 @@ const HomeScreen = ({ navigation }) => { const handleCreateGroup = async () => { if (!newGroupName) { - Alert.alert("Error", "Please enter a group name."); + showToast("Please enter a group name.", "error"); return; } setIsCreatingGroup(true); @@ -123,10 +125,11 @@ const HomeScreen = ({ navigation }) => { await createGroup(newGroupName); hideModal(); setNewGroupName(""); + showToast("Group created successfully!", "success"); await fetchGroups(); // Refresh the groups list } catch (error) { console.error("Failed to create group:", error); - Alert.alert("Error", "Failed to create group."); + showToast("Failed to create group.", "error"); } finally { setIsCreatingGroup(false); } diff --git a/mobile/screens/LoginScreen.js b/mobile/screens/LoginScreen.js index 194db5bb..594ed264 100644 --- a/mobile/screens/LoginScreen.js +++ b/mobile/screens/LoginScreen.js @@ -1,25 +1,29 @@ import React, { useState, useContext } from 'react'; -import { View, StyleSheet, Alert } from 'react-native'; +import { View, StyleSheet } from 'react-native'; import { Text, TextInput } from 'react-native-paper'; import HapticButton from '../components/ui/HapticButton'; import { AuthContext } from '../context/AuthContext'; +import { useToast } from '../context/ToastContext'; const LoginScreen = ({ navigation }) => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [isLoading, setIsLoading] = useState(false); const { login } = useContext(AuthContext); + const { showToast } = useToast(); const handleLogin = async () => { if (!email || !password) { - Alert.alert('Error', 'Please enter both email and password.'); + showToast('Please enter both email and password.', 'error'); return; } setIsLoading(true); const success = await login(email, password); setIsLoading(false); if (!success) { - Alert.alert('Login Failed', 'Invalid email or password. Please try again.'); + showToast('Invalid email or password. Please try again.', 'error'); + } else { + showToast('Logged in successfully!', 'success'); } }; diff --git a/mobile/screens/SignupScreen.js b/mobile/screens/SignupScreen.js index d40f3627..c0619766 100644 --- a/mobile/screens/SignupScreen.js +++ b/mobile/screens/SignupScreen.js @@ -1,8 +1,9 @@ import React, { useState, useContext } from 'react'; -import { View, StyleSheet, Alert } from 'react-native'; +import { View, StyleSheet, ScrollView } from 'react-native'; import { Text, TextInput } from 'react-native-paper'; import HapticButton from '../components/ui/HapticButton'; import { AuthContext } from '../context/AuthContext'; +import { useToast } from '../context/ToastContext'; const SignupScreen = ({ navigation }) => { const [name, setName] = useState(''); @@ -11,27 +12,25 @@ const SignupScreen = ({ navigation }) => { const [confirmPassword, setConfirmPassword] = useState(''); const [isLoading, setIsLoading] = useState(false); const { signup } = useContext(AuthContext); + const { showToast } = useToast(); const handleSignup = async () => { if (!name || !email || !password || !confirmPassword) { - Alert.alert('Error', 'Please fill in all fields.'); + showToast('Please fill in all fields.', 'error'); return; } if (password !== confirmPassword) { - Alert.alert('Error', "Passwords don't match!"); + showToast("Passwords don't match!", 'error'); return; } setIsLoading(true); const success = await signup(name, email, password); setIsLoading(false); if (success) { - Alert.alert( - 'Success', - 'Your account has been created successfully. Please log in.', - [{ text: 'OK', onPress: () => navigation.navigate('Login') }] - ); + showToast('Account created successfully! Please login.', 'success'); + navigation.navigate('Login'); } else { - Alert.alert('Signup Failed', 'An error occurred. Please try again.'); + showToast('Signup failed. An error occurred. Please try again.', 'error'); } };