Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .Jules/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
5 changes: 5 additions & 0 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
5 changes: 4 additions & 1 deletion mobile/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<AuthProvider>
<PaperProvider>
<AppNavigator />
<ToastProvider>
<AppNavigator />
</ToastProvider>
</PaperProvider>
</AuthProvider>
);
Expand Down
73 changes: 73 additions & 0 deletions mobile/context/ToastContext.js
Original file line number Diff line number Diff line change
@@ -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 (
<ToastContext.Provider value={{ showToast }}>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Memoize provider value to avoid app-wide rerenders on each toast state change.

At Line 47, value={{ showToast }} creates a new object every render, forcing all useToast() consumers to rerender whenever toast UI state changes.

♻️ Proposed fix
 import React, { createContext, useState, useContext, useCallback } from 'react';
+import { useMemo } from 'react';
@@
-  return (
-    <ToastContext.Provider value={{ showToast }}>
+  const contextValue = useMemo(() => ({ showToast }), [showToast]);
+
+  return (
+    <ToastContext.Provider value={contextValue}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/context/ToastContext.js` at line 47, The provider is recreating its
value object every render which forces all useToast() consumers to rerender;
inside the component that renders ToastContext.Provider, memoize the value by
creating const value = useMemo(() => ({ showToast }), [showToast]) and pass
value into <ToastContext.Provider value={value}>; also ensure showToast itself
is stable (wrap its definition in useCallback if it’s recreated each render) so
the memo actually prevents unnecessary rerenders.

{children}
<Snackbar
visible={visible}
onDismiss={hideToast}
duration={3000}
style={[styles.snackbar, { backgroundColor: getBackgroundColor() }]}
action={{
label: 'Dismiss',
onPress: hideToast,
labelStyle: { color: getTextColor() },
}}
theme={{ colors: { onSurface: getTextColor(), surface: getBackgroundColor() } }}
>
{message}
</Snackbar>
</ToastContext.Provider>
);
};

export const useToast = () => useContext(ToastContext);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's explore the mobile/context directory structure
find mobile/context -type f -name "*.js" -o -name "*.ts" -o -name "*.tsx" 2>/dev/null | head -20

Repository: Devasy/splitwiser

Length of output: 121


🏁 Script executed:

# Check the exact content of ToastContext.js
cat -n mobile/context/ToastContext.js

Repository: Devasy/splitwiser

Length of output: 2427


🏁 Script executed:

# Search for ToastProvider to understand the context setup
rg "ToastProvider" -A 5 -B 2

Repository: Devasy/splitwiser

Length of output: 3168


🏁 Script executed:

# Check if other context hooks in the codebase follow the guard pattern
rg "useContext\(" mobile/context web/contexts 2>/dev/null | head -20

Repository: Devasy/splitwiser

Length of output: 445


🏁 Script executed:

# Look for other custom context hooks to see the pattern
ast-grep --pattern 'export const use$_$_ = () => useContext($_);'

Repository: Devasy/splitwiser

Length of output: 43


Guard useToast to fail fast when provider is missing.

At Line 67, returning raw context can cause runtime crashes (Cannot destructure property 'showToast') if used outside ToastProvider. Throw a clear error from the hook instead. This pattern is already implemented in the web version (web/contexts/ToastContext.tsx) and should be applied consistently here.

🛡️ Proposed fix
-export const useToast = () => useContext(ToastContext);
+export const useToast = () => {
+  const context = useContext(ToastContext);
+  if (!context) {
+    throw new Error('useToast must be used within a ToastProvider');
+  }
+  return context;
+};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const useToast = () => useContext(ToastContext);
export const useToast = () => {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/context/ToastContext.js` at line 67, The hook useToast currently
returns the raw ToastContext value which leads to obscure runtime errors when
used outside a ToastProvider; update useToast to call useContext(ToastContext),
check if the returned value is undefined/null, and if so throw a clear Error
like "useToast must be used within a ToastProvider" so consumers fail fast;
reference the existing symbol useToast and ToastContext (and the ToastProvider
pattern used elsewhere) when making the change.


const styles = StyleSheet.create({
snackbar: {
marginBottom: 80, // Avoid bottom nav bar
},
});
11 changes: 7 additions & 4 deletions mobile/screens/HomeScreen.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
Expand All @@ -115,18 +117,19 @@ 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);
try {
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);
}
Expand Down
10 changes: 7 additions & 3 deletions mobile/screens/LoginScreen.js
Original file line number Diff line number Diff line change
@@ -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');
}
};

Expand Down
17 changes: 8 additions & 9 deletions mobile/screens/SignupScreen.js
Original file line number Diff line number Diff line change
@@ -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('');
Expand All @@ -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');
Comment on lines +30 to +31
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Prefer replace('Login') after successful signup to avoid stale back navigation.

At Line 31, navigate('Login') can keep Signup in history; users may return to a completed form via back gesture. replace is usually a better post-signup transition.

♻️ Proposed fix
     if (success) {
       showToast('Account created successfully! Please login.', 'success');
-      navigation.navigate('Login');
+      navigation.replace('Login');
     } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
showToast('Account created successfully! Please login.', 'success');
navigation.navigate('Login');
showToast('Account created successfully! Please login.', 'success');
navigation.replace('Login');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/screens/SignupScreen.js` around lines 30 - 31, The post-signup
navigation uses navigation.navigate('Login') which leaves the SignupScreen in
the history and can allow back navigation to a completed form; change the
transition to use navigation.replace('Login') where the call is made (replace
the navigation.navigate call in the success path after showToast in
SignupScreen.js) so the SignupScreen is removed from the stack and users cannot
go back to the completed signup screen.

} else {
Alert.alert('Signup Failed', 'An error occurred. Please try again.');
showToast('Signup failed. An error occurred. Please try again.', 'error');
}
};

Expand Down
Loading