diff --git a/.Jules/changelog.md b/.Jules/changelog.md
index 11fc864..0f04b65 100644
--- a/.Jules/changelog.md
+++ b/.Jules/changelog.md
@@ -123,3 +123,8 @@
- `.jules/todo.md`
- `.jules/knowledge.md`
- `.jules/changelog.md`
+
+### 2026-03-08
+- Added generic `Skeleton` component and `GroupListSkeleton` component to mobile app
+- Replaced basic ActivityIndicator loading screen with `GroupListSkeleton` on mobile `HomeScreen`
+- Refactored `FriendsScreen` inline skeleton logic to use the new generic `Skeleton` component
diff --git a/.Jules/knowledge.md b/.Jules/knowledge.md
index 43a9ab0..a5062c3 100644
--- a/.Jules/knowledge.md
+++ b/.Jules/knowledge.md
@@ -756,3 +756,8 @@ _Document errors and their solutions here as you encounter them._
- react-native-paper: UI components
- axios: API calls (via api/client.js)
- expo: Platform SDK
+
+### Development Workflows
+- The mobile project is configured with `react-native-web`. You can test it in a browser using `npx expo start --web`.
+- Use `AsyncStorage` values (`auth_token`, `refresh_token`, `user_data`) directly into Playwright's `window.localStorage` to bypass login flows for Playwright testing via react-native-web.
+- When creating files in new subdirectories inside `/mobile/`, use `mkdir -p` before the write operation to ensure the path exists.
diff --git a/.Jules/todo.md b/.Jules/todo.md
index ebb0c7a..326aef8 100644
--- a/.Jules/todo.md
+++ b/.Jules/todo.md
@@ -57,8 +57,9 @@
- Impact: Native feel, users can easily refresh data
- Size: ~150 lines
-- [ ] **[ux]** Complete skeleton loading for HomeScreen groups
- - File: `mobile/screens/HomeScreen.js`
+- [x] **[ux]** Complete skeleton loading for HomeScreen groups
+ - Completed: 2026-03-08
+ - File: `mobile/screens/HomeScreen.js`, `mobile/components/ui/Skeleton.js`, `mobile/components/skeletons/GroupListSkeleton.js`
- Context: Replace ActivityIndicator with skeleton group cards
- Impact: Better loading experience, less jarring
- Size: ~40 lines
@@ -168,3 +169,7 @@
- Completed: 2026-02-08
- Files modified: `web/components/ui/PasswordStrength.tsx`, `web/pages/Auth.tsx`
- Impact: Provides visual feedback on password complexity during signup
+- [x] **[ux]** Complete skeleton loading for HomeScreen groups
+ - Completed: 2026-03-08
+ - Files modified: `mobile/screens/HomeScreen.js`, `mobile/screens/FriendsScreen.js`, `mobile/components/ui/Skeleton.js`, `mobile/components/skeletons/GroupListSkeleton.js`
+ - Impact: Better loading experience across the mobile app via generic skeleton components.
diff --git a/mobile/components/skeletons/GroupListSkeleton.js b/mobile/components/skeletons/GroupListSkeleton.js
new file mode 100644
index 0000000..02d0d1e
--- /dev/null
+++ b/mobile/components/skeletons/GroupListSkeleton.js
@@ -0,0 +1,44 @@
+import React from 'react';
+import { StyleSheet, View } from 'react-native';
+import { Card } from 'react-native-paper';
+import Skeleton from '../ui/Skeleton';
+
+const GroupListSkeleton = ({ count = 5 }) => {
+ return (
+
+ {Array.from({ length: count }).map((_, index) => (
+
+ }
+ left={(props) => (
+
+ )}
+ />
+
+
+
+
+ ))}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ padding: 16,
+ },
+ card: {
+ marginBottom: 16,
+ },
+});
+
+export default GroupListSkeleton;
diff --git a/mobile/components/ui/Skeleton.js b/mobile/components/ui/Skeleton.js
new file mode 100644
index 0000000..e66aafd
--- /dev/null
+++ b/mobile/components/ui/Skeleton.js
@@ -0,0 +1,51 @@
+import React, { useEffect, useRef } from 'react';
+import { Animated, StyleSheet, View } from 'react-native';
+import { useTheme } from 'react-native-paper';
+
+const Skeleton = ({ width, height, borderRadius = 4, style }) => {
+ const theme = useTheme();
+ const opacityAnim = useRef(new Animated.Value(0.3)).current;
+
+ useEffect(() => {
+ const loop = Animated.loop(
+ Animated.sequence([
+ Animated.timing(opacityAnim, {
+ toValue: 1,
+ duration: 700,
+ useNativeDriver: true,
+ }),
+ Animated.timing(opacityAnim, {
+ toValue: 0.3,
+ duration: 700,
+ useNativeDriver: true,
+ }),
+ ])
+ );
+ loop.start();
+ return () => loop.stop();
+ }, [opacityAnim]);
+
+ return (
+
+ );
+};
+
+const styles = StyleSheet.create({
+ skeleton: {
+ overflow: 'hidden',
+ },
+});
+
+export default Skeleton;
diff --git a/mobile/screens/FriendsScreen.js b/mobile/screens/FriendsScreen.js
index c778a95..3c8b87c 100644
--- a/mobile/screens/FriendsScreen.js
+++ b/mobile/screens/FriendsScreen.js
@@ -1,6 +1,6 @@
import { useIsFocused } from "@react-navigation/native";
-import { useContext, useEffect, useRef, useState } from "react";
-import { Alert, Animated, FlatList, RefreshControl, StyleSheet, View } from "react-native";
+import { useContext, useEffect, useState } from "react";
+import { Alert, FlatList, RefreshControl, StyleSheet, View } from "react-native";
import {
Appbar,
Avatar,
@@ -15,6 +15,7 @@ import { triggerPullRefreshHaptic } from '../components/ui/hapticUtils';
import { getFriendsBalance, getGroups } from "../api/groups";
import { AuthContext } from "../context/AuthContext";
import { formatCurrency } from "../utils/currency";
+import Skeleton from "../components/ui/Skeleton";
const FriendsScreen = () => {
const { token, user } = useContext(AuthContext);
@@ -167,42 +168,12 @@ const FriendsScreen = () => {
);
};
- // Shimmer skeleton components
- const opacityAnim = useRef(new Animated.Value(0.3)).current;
- useEffect(() => {
- const loop = Animated.loop(
- Animated.sequence([
- Animated.timing(opacityAnim, {
- toValue: 1,
- duration: 700,
- useNativeDriver: true,
- }),
- Animated.timing(opacityAnim, {
- toValue: 0.3,
- duration: 700,
- useNativeDriver: true,
- }),
- ])
- );
- loop.start();
- return () => loop.stop();
- }, [opacityAnim]);
-
const SkeletonRow = () => (
-
+
-
-
+
+
);
@@ -315,23 +286,6 @@ const styles = StyleSheet.create({
alignItems: "center",
marginBottom: 14,
},
- skeletonAvatar: {
- width: 48,
- height: 48,
- borderRadius: 24,
- backgroundColor: "#e0e0e0",
- },
- skeletonLine: {
- height: 14,
- backgroundColor: "#e0e0e0",
- borderRadius: 6,
- marginBottom: 6,
- },
- skeletonLineSmall: {
- height: 12,
- backgroundColor: "#e0e0e0",
- borderRadius: 6,
- },
});
export default FriendsScreen;
diff --git a/mobile/screens/HomeScreen.js b/mobile/screens/HomeScreen.js
index d2f3c38..7f5cf14 100644
--- a/mobile/screens/HomeScreen.js
+++ b/mobile/screens/HomeScreen.js
@@ -1,7 +1,6 @@
import { useContext, useEffect, useState } from "react";
import { Alert, FlatList, RefreshControl, StyleSheet, View } from "react-native";
import {
- ActivityIndicator,
Appbar,
Avatar,
Modal,
@@ -17,6 +16,7 @@ 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 GroupListSkeleton from "../components/skeletons/GroupListSkeleton";
const HomeScreen = ({ navigation }) => {
const { token, logout, user } = useContext(AuthContext);
@@ -257,9 +257,7 @@ const HomeScreen = ({ navigation }) => {
{isLoading ? (
-
-
-
+
) : (