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
124 changes: 2 additions & 122 deletions .Jules/changelog.md
Original file line number Diff line number Diff line change
@@ -1,125 +1,5 @@
# Splitwiser UI/UX Changelog

> All UI/UX changes made by Jules automated enhancement agent.

---
# Changelog

## [Unreleased]
- **[ux] Mobile**: Added biometric authentication option (FaceID/TouchID) to enable quicker and more secure logins for returning users. Implemented the feature directly in `AuthContext` with a toggle in `AccountScreen` and a fallback/login button in `LoginScreen`.
Comment on lines 3 to +4
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

Add the required blank line after the heading.

markdownlint flags ## [Unreleased] because it is not followed by a blank line.

🧹 Proposed fix
 ## [Unreleased]
+
 - **[ux] Mobile**: Added biometric authentication option (FaceID/TouchID) to enable quicker and more secure logins for returning users. Implemented the feature directly in `AuthContext` with a toggle in `AccountScreen` and a fallback/login button in `LoginScreen`.
📝 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
## [Unreleased]
- **[ux] Mobile**: Added biometric authentication option (FaceID/TouchID) to enable quicker and more secure logins for returning users. Implemented the feature directly in `AuthContext` with a toggle in `AccountScreen` and a fallback/login button in `LoginScreen`.
## [Unreleased]
- **[ux] Mobile**: Added biometric authentication option (FaceID/TouchID) to enable quicker and more secure logins for returning users. Implemented the feature directly in `AuthContext` with a toggle in `AccountScreen` and a fallback/login button in `LoginScreen`.
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 3-3: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.Jules/changelog.md around lines 3 - 4, The markdown heading "##
[Unreleased]" is missing a blank line after it which triggers markdownlint; open
the changelog entry containing the heading "## [Unreleased]" and insert a single
blank line immediately following that heading so the paragraph/list that follows
is separated by one empty line.


### 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 (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).
- Added `accessibilityRole` to ensure screen readers identify element types correctly.
- Added `accessibilityHint` for clearer context on destructive actions or complex interactions.
- Covered Auth, Dashboard, Groups, and Utility screens.
- **Technical:** Updated all files in `mobile/screens/` to compliant with React Native accessibility standards.

- **Mobile Pull-to-Refresh:** Implemented native pull-to-refresh interactions with haptic feedback for key lists.
- **Features:**
- Integrated `RefreshControl` into `HomeScreen`, `FriendsScreen`, and `GroupDetailsScreen`.
- Added haptic feedback (`Haptics.ImpactFeedbackStyle.Light`) on refresh trigger.
- Separated 'isRefreshing' state from 'isLoading' to prevent full-screen spinner interruptions.
- Themed the refresh spinner using `react-native-paper`'s primary color.
- **Technical:** Installed `expo-haptics`. Refactored data fetching logic to support silent updates.

- **Confirmation Dialog System:** Replaced browser's native `alert`/`confirm` with a custom, accessible, and themed modal system.
- **Features:**
- Dual-theme support (Glassmorphism & Neobrutalism).
- Asynchronous `useConfirm` hook returning a Promise.
- Specialized variants (danger, warning, info) with appropriate styling and icons.
- Fully accessible `Modal` component (added `role="dialog"`, `aria-labelledby`, `aria-modal`).
- **Technical:** Created `web/components/ui/ConfirmDialog.tsx`, `web/contexts/ConfirmContext.tsx`. Updated `web/pages/GroupDetails.tsx` to use the new system.

- **Error Boundary System:** Implemented a global React Error Boundary to catch render errors gracefully.
- **Features:**
- Dual-theme support (Glassmorphism & Neobrutalism) for the error fallback UI.
- "Retry" button to reset error state and re-render.
- "Home" button to navigate back to safety.
- Captures errors in `AppRoutes` and displays a user-friendly message instead of a white screen.
- **Technical:** Created `web/components/ErrorBoundary.tsx` using a hybrid Class+Functional approach to support Hooks in the fallback UI. Integrated into `web/App.tsx`.

- Inline form validation in Auth page with real-time feedback and proper ARIA accessibility support (`aria-invalid`, `aria-describedby`, `role="alert"`).
- Dashboard skeleton loading state (`DashboardSkeleton`) to improve perceived performance during data fetch.
- Comprehensive `EmptyState` component for Groups and Friends pages to better guide new users.
- Toast notification system (`ToastContext`, `Toast` component) for providing non-blocking user feedback.
- Keyboard navigation support for Groups page, enabling accessibility for power users.

### Changed
- **Web App:** Refactored `GroupDetails` destructive actions (Delete Group, Delete Expense, Leave Group, Remove Member) to use the new `ConfirmDialog` instead of `window.confirm`.
- **Accessibility:** Updated `Modal` component to include proper ARIA roles and labels, fixing a long-standing accessibility gap.
- Updated JULES_PROMPT.md based on review of successful PRs:
- Emphasized complete system implementation over piecemeal changes
- Added best practices from successful PRs (Toast system, keyboard navigation iteration)
- Refined task categories to focus on complete features
- Enhanced validation checklist
- Added implementation phases guide
- Documented successful patterns to repeat

### Planned
- See `todo.md` for queued tasks

---

## [2026-01-13] - Prompt Optimization

### Changed
- **Streamlined JULES_PROMPT.md** (336 → 179 lines, 47% reduction):
- Removed completed task lists (moved to changelog/knowledge)
- Removed redundant "best practices" examples
- Consolidated duplicate sections
- Kept only actionable guidance
- **Reviewed merged PRs** (#227, #236, #234, #226, #225)
- **Updated knowledge.md** with detailed PR reviews and successful patterns

**Key Insights:**
1. Complete systems > piecemeal changes
2. Accessibility and theme support from the start
3. Semantic HTML over manual ARIA when possible
4. Multiple commits in PRs were often from review feedback, not agent iteration

**Files Modified:**
- `.jules/JULES_PROMPT.md` (streamlined and optimized)
- `.jules/knowledge.md` (added PR review section)
- `.jules/changelog.md`

---

## [2026-01-01] - Initial Setup

### Added
- Created Jules agent documentation and tracking system
- `.Jules/JULES_PROMPT.md` - Main agent instructions
- `.Jules/todo.md` - Task queue with prioritized improvements
- `.Jules/knowledge.md` - Codebase knowledge base
- `.Jules/changelog.md` - This changelog

### Analysis Completed
- Full audit of `web/` application structure
- Full audit of `mobile/` application structure
- Identified accessibility gaps
- Identified UX improvement opportunities
- Documented theming system patterns
- Documented component APIs

**Files Created:**
- `.jules/JULES_PROMPT.md`
- `.jules/todo.md`
- `.jules/knowledge.md`
- `.jules/changelog.md`
8 changes: 4 additions & 4 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,12 @@

### Mobile

- [ ] **[ux]** Biometric authentication option
- Files: `mobile/context/AuthContext.js`, add local auth
- [x] **[ux]** Biometric authentication option
- Completed: 2026-04-19
- Files: `mobile/context/AuthContext.js`, `mobile/screens/AccountScreen.js`, `mobile/screens/LoginScreen.js`
- Context: FaceID/TouchID for quick login
- Impact: Faster, more secure login
- Size: ~70 lines
- Added: 2026-01-01
- Size: ~100 lines

---

Expand Down
120 changes: 111 additions & 9 deletions mobile/context/AuthContext.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { createContext, useEffect, useState } from "react";
import * as LocalAuthentication from "expo-local-authentication";
import * as authApi from "../api/auth";
import {
clearAuthTokens,
Expand All @@ -14,30 +15,60 @@ export const AuthProvider = ({ children }) => {
const [token, setToken] = useState(null);
const [refresh, setRefresh] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [isBiometricEnabled, setIsBiometricEnabled] = useState(false);
const [storedCredentials, setStoredCredentials] = useState(null);

// Load token and user data from AsyncStorage on app start
useEffect(() => {
const loadStoredAuth = async () => {
try {
const storedToken = await AsyncStorage.getItem("auth_token");
const storedRefresh = await AsyncStorage.getItem("refresh_token");
const storedUser = await AsyncStorage.getItem("user_data");
const storedUser = await AsyncStorage.getItem("user_data");
const biometricPref = await AsyncStorage.getItem("biometric_enabled");

const isBiometric = biometricPref === "true";
setIsBiometricEnabled(isBiometric);

if (storedToken && storedUser) {
setToken(storedToken);
setRefresh(storedRefresh);
await setAuthTokens({
newAccessToken: storedToken,
newRefreshToken: storedRefresh,
});
// Normalize user id shape: ensure `_id` exists even if API stored `id`
// Normalize user id shape
const parsed = JSON.parse(storedUser);
const normalized = parsed?._id
? parsed
: parsed?.id
? { ...parsed, _id: parsed.id }
: parsed;
setUser(normalized);

setStoredCredentials({
token: storedToken,
refresh: storedRefresh,
user: normalized
});

if (isBiometric) {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: "Authenticate to login",
fallbackLabel: "Use Password",
});
Comment on lines 25 to +52
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether auth/biometric secrets are still stored in AsyncStorage and whether secure storage is configured.
# Expectation: Token/refresh credential storage for biometric login should use secure storage, not plain AsyncStorage.

set -euo pipefail

echo "Dependencies mentioning secure storage:"
jq -r '.dependencies // {} | to_entries[] | select(.key | test("secure|keychain|keystore"; "i")) | "\(.key): \(.value)"' mobile/package.json 2>/dev/null || true

echo
echo "Auth storage usage:"
rg -n -C 3 'AsyncStorage\.(getItem|setItem|removeItem)\("(auth_token|refresh_token|user_data|biometric_enabled)"|expo-secure-store|SecureStore|requireAuthentication|Keychain' .

Repository: Devasy/splitwiser

Length of output: 7840


Store authentication tokens in secure storage, not AsyncStorage.

The code stores auth_token and refresh_token in plain AsyncStorage (lines 25-26, 96, 112). These credentials must be stored using a platform secure store (e.g., expo-secure-store, Keychain, Keystore); AsyncStorage is unencrypted and vulnerable to physical device access. Additionally, the biometric prompt only gates React state restoration—it does not prevent access to the stored credential material itself. Migrate token storage to secure storage and keep AsyncStorage for non-sensitive preferences only.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/context/AuthContext.js` around lines 25 - 52, The code currently
reads/writes sensitive tokens using AsyncStorage.getItem/setItem (auth_token,
refresh_token) and then uses LocalAuthentication.authenticateAsync only as a UI
gate; replace all AsyncStorage uses for auth_token and refresh_token with a
platform secure store (e.g., expo-secure-store) API
(SecureStore.getItemAsync/SecureStore.setItemAsync) across this module and any
other places storing tokens (currently around setStoredCredentials usage and
other save paths referenced), keep AsyncStorage only for non-sensitive prefs
like biometric_enabled; also change the restore flow in the function that calls
LocalAuthentication.authenticateAsync so biometrics are performed before
loading/setting in-memory credentials (i.e., call
LocalAuthentication.authenticateAsync first, then retrieve tokens from
SecureStore and call setStoredCredentials with token, refresh and normalized
user).


if (result.success) {
setToken(storedToken);
setRefresh(storedRefresh);
await setAuthTokens({
newAccessToken: storedToken,
newRefreshToken: storedRefresh,
});
setUser(normalized);
}
} else {
setToken(storedToken);
setRefresh(storedRefresh);
await setAuthTokens({
newAccessToken: storedToken,
newRefreshToken: storedRefresh,
});
setUser(normalized);
}
}
} catch (error) {
console.error("Failed to load stored authentication:", error);
Expand Down Expand Up @@ -106,6 +137,69 @@ export const AuthProvider = ({ children }) => {
saveUser();
}, [user]);

const authenticateBiometric = async () => {
if (!storedCredentials) return false;

try {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: "Authenticate to login",
fallbackLabel: "Use Password",
});

if (result.success) {
setToken(storedCredentials.token);
setRefresh(storedCredentials.refresh);
await setAuthTokens({
newAccessToken: storedCredentials.token,
newRefreshToken: storedCredentials.refresh,
});
setUser(storedCredentials.user);
return true;
}
} catch (error) {
console.error("Biometric authentication failed:", error);
}
return false;
};

const enableBiometric = async () => {
try {
const isEnrolled = await LocalAuthentication.isEnrolledAsync();
if (!isEnrolled) {
return { success: false, error: "No biometrics enrolled on this device." };
}

const result = await LocalAuthentication.authenticateAsync({
promptMessage: "Authenticate to enable biometric login",
});

if (result.success) {
await AsyncStorage.setItem("biometric_enabled", "true");
setIsBiometricEnabled(true);
// Also save current credentials if not already stored
if (token && user) {
setStoredCredentials({ token, refresh, user });
}
return { success: true };
}
return { success: false, error: "Authentication failed." };
} catch (error) {
console.error("Failed to enable biometrics:", error);
return { success: false, error: "An error occurred." };
}
};

const disableBiometric = async () => {
try {
await AsyncStorage.setItem("biometric_enabled", "false");
setIsBiometricEnabled(false);
return { success: true };
} catch (error) {
console.error("Failed to disable biometrics:", error);
return { success: false, error: "An error occurred." };
}
};

const login = async (email, password) => {
try {
const response = await authApi.login(email, password);
Expand Down Expand Up @@ -159,6 +253,9 @@ export const AuthProvider = ({ children }) => {
setToken(null);
setRefresh(null);
setUser(null);
setStoredCredentials(null);
await AsyncStorage.removeItem("biometric_enabled");
setIsBiometricEnabled(false);
await clearAuthTokens();
};

Expand All @@ -182,6 +279,11 @@ export const AuthProvider = ({ children }) => {
signup,
logout,
updateUserInContext,
isBiometricEnabled,
enableBiometric,
disableBiometric,
authenticateBiometric,
storedCredentials,
}}
>
{children}
Expand Down
13 changes: 13 additions & 0 deletions mobile/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"expo": "^54.0.25",
"expo-haptics": "~15.0.8",
"expo-image-picker": "~17.0.8",
"expo-local-authentication": "~17.0.8",
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect Expo app config files for expo-local-authentication plugin / Face ID permission.
# Expectation: An app config contains expo-local-authentication and/or NSFaceIDUsageDescription/faceIDPermission.

set -euo pipefail

fd -i '^(app|app\.config)\.(json|js|ts)$|package\.json$' . | while read -r file; do
  echo "----- $file -----"
  rg -n -C 4 'expo-local-authentication|faceIDPermission|NSFaceIDUsageDescription|plugins|ios' "$file" || true
done

Repository: Devasy/splitwiser

Length of output: 1760


🏁 Script executed:

#!/bin/bash
# Check complete mobile/app.json file
echo "=== Full mobile/app.json ===" 
wc -l mobile/app.json
echo ""
cat -n mobile/app.json

Repository: Devasy/splitwiser

Length of output: 1388


🏁 Script executed:

#!/bin/bash
# Search for LocalAuthentication usage in the codebase
echo "=== Searching for LocalAuthentication usage ==="
rg -n 'LocalAuthentication|authenticateAsync|isEnrolledAsync|local-authentication' mobile/ --type js --type ts

Repository: Devasy/splitwiser

Length of output: 618


Add NSFaceIDUsageDescription to the iOS infoPlist configuration.

The dependency is being added, and LocalAuthentication is actively used in mobile/context/AuthContext.js. However, mobile/app.json is missing the required Face ID permission. Without NSFaceIDUsageDescription in the iOS infoPlist, Face ID will not function properly on iOS. Add the permission description to the existing infoPlist in the iOS section:

"infoPlist": {
  "NSPhotoLibraryUsageDescription": "Allow Splitwiser to select a group icon from your photo library.",
  "NSFaceIDUsageDescription": "Allow Splitwiser to use Face ID for authentication."
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/package.json` at line 21, The iOS Info.plist is missing the
NSFaceIDUsageDescription required by expo-local-authentication; update the iOS
section of your app.json (where the existing infoPlist lives) to add the key
"NSFaceIDUsageDescription" with a user-facing message such as "Allow Splitwiser
to use Face ID for authentication." Ensure the infoPlist object alongside
"NSPhotoLibraryUsageDescription" includes this new key so LocalAuthentication
used in mobile/context/AuthContext.js can access Face ID on iOS.

"expo-status-bar": "~3.0.8",
"react": "19.1.0",
"react-dom": "19.1.0",
Expand Down
Loading
Loading