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
9 changes: 9 additions & 0 deletions .Jules/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
## [Unreleased]

### Added
- **Mobile Skeleton Loading:** Implemented a skeleton loading state for the mobile Home screen.
- **Features:**
- Replaced `ActivityIndicator` with a pulsing skeleton list that mimics the actual group cards.
- Created reusable `Skeleton` component using `Animated` API for performant native driver animations.
- Created `GroupListSkeleton` component matching the exact layout of `HomeScreen` cards.
- Dual-theme support via `react-native-paper` theme colors.
- Added accessibility support (`accessibilityRole="progressbar"`, `accessibilityLabel="Loading groups list"`).
- **Technical:** Created `mobile/components/ui/Skeleton.js`, `mobile/components/skeletons/GroupListSkeleton.js`. Updated `mobile/screens/HomeScreen.js`.

- **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
32 changes: 30 additions & 2 deletions .Jules/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,36 @@ Commonly used components:
- `<Portal>` and `<Modal>` for overlays
- `<ActivityIndicator>` for loading states

### Mobile Skeleton Loading Pattern

**Date:** 2026-02-13
**Context:** Creating performant skeleton screens in React Native

To create a pulsing skeleton effect without external libraries:

```javascript
// 1. Create Animated Value
const opacity = useRef(new Animated.Value(0.3)).current;

// 2. Loop Animation
useEffect(() => {
const anim = Animated.loop(
Animated.sequence([
Animated.timing(opacity, { toValue: 0.7, duration: 800, useNativeDriver: true }),
Animated.timing(opacity, { toValue: 0.3, duration: 800, useNativeDriver: true }),
])
);
anim.start();
return () => anim.stop();
}, []);

// 3. Render Animated.View
<Animated.View style={{ opacity, backgroundColor: theme.colors.surfaceVariant, ... }} />
```

**Accessibility:**
Always wrap the skeleton container with `accessibilityRole="progressbar"` and a descriptive `accessibilityLabel` so screen readers know content is loading.

### Safe Area Pattern

**Date:** 2026-01-01
Expand Down Expand Up @@ -578,8 +608,6 @@ _Document errors and their solutions here as you encounter them._
- Verification requires triggering an error during render (e.g., conditional throw).
- Must wrap Router if using `useNavigate` or `Link` in fallback.

---

### ✅ Successful PR Pattern: Toast Notification System (#227)

**Date:** 2026-01-13
Expand Down
9 changes: 5 additions & 4 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,12 @@
- Impact: Native feel, users can easily refresh data
- Size: ~150 lines

- [ ] **[ux]** Complete skeleton loading for HomeScreen groups
- File: `mobile/screens/HomeScreen.js`
- Context: Replace ActivityIndicator with skeleton group cards
- [x] **[ux]** Complete skeleton loading for HomeScreen groups
- Completed: 2026-02-13
- Files: `mobile/screens/HomeScreen.js`, `mobile/components/ui/Skeleton.js`, `mobile/components/skeletons/GroupListSkeleton.js`
- Context: Replaced ActivityIndicator with skeleton group cards
- Impact: Better loading experience, less jarring
- Size: ~40 lines
- Size: ~60 lines
- Added: 2026-01-01

- [x] **[a11y]** Complete accessibility labels for all screens
Expand Down
39 changes: 39 additions & 0 deletions mobile/components/skeletons/GroupListSkeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { Card } from 'react-native-paper';
import Skeleton from '../ui/Skeleton';

const GroupListSkeleton = () => {
return (
<View
style={styles.list}
accessibilityRole="progressbar"
accessibilityLabel="Loading groups list"
>
{[1, 2, 3, 4, 5].map((key) => (
<Card key={key} style={styles.card}>
<Card.Title
title={<Skeleton width={120} height={20} />}
left={(props) => (
<Skeleton width={40} height={40} borderRadius={20} style={props.style} />
)}
/>
Comment on lines +14 to +20
Copy link
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:

# First, check if react-native-paper is in node_modules and locate Card.Title
fd -t f "CardTitle" --path "*node_modules*" 2>/dev/null | head -20

Repository: Devasy/splitwiser

Length of output: 43


🏁 Script executed:

# Also check for package.json to understand which version of react-native-paper is used
find . -maxdepth 3 -name "package.json" -type f ! -path "*/node_modules/*" -exec grep -l "react-native-paper" {} \;

Repository: Devasy/splitwiser

Length of output: 82


🏁 Script executed:

# Search for the Skeleton component referenced in GroupListSkeleton.js
fd "Skeleton.js" -t f ! -path "*/node_modules/*"

Repository: Devasy/splitwiser

Length of output: 229


🏁 Script executed:

cat mobile/package.json | grep -A 5 -B 5 "react-native-paper"

Repository: Devasy/splitwiser

Length of output: 395


🏁 Script executed:

fd "Skeleton.js" --type f ! --path "*/node_modules/*"

Repository: Devasy/splitwiser

Length of output: 294


🏁 Script executed:

cat -n mobile/components/skeletons/GroupListSkeleton.js

Repository: Devasy/splitwiser

Length of output: 1288


🏁 Script executed:

find . -name "Skeleton.js" -type f | grep -v node_modules

Repository: Devasy/splitwiser

Length of output: 95


🏁 Script executed:

cat -n mobile/ui/Skeleton.js

Repository: Devasy/splitwiser

Length of output: 114


🏁 Script executed:

# Check if there are any other usages of Card.Title with JSX elements as title
rg "Card.Title" --type jsx --type js -A 3 mobile/

Repository: Devasy/splitwiser

Length of output: 88


🏁 Script executed:

cat -n mobile/components/ui/Skeleton.js

Repository: Devasy/splitwiser

Length of output: 1409


🏁 Script executed:

# Check Card.Title usage elsewhere
rg "Card.Title" -A 3 mobile/

Repository: Devasy/splitwiser

Length of output: 2609


🏁 Script executed:

# Check if there are TypeScript errors or type issues reported
find . -name "tsconfig.json" -type f ! -path "*/node_modules/*" -exec cat {} \;

Repository: Devasy/splitwiser

Length of output: 603


🏁 Script executed:

# Look for any error logs or build warnings related to GroupListSkeleton
rg "GroupListSkeleton" --type js

Repository: Devasy/splitwiser

Length of output: 389


🏁 Script executed:

# Check the exact Card.Title prop types in node_modules react-native-paper
find ./node_modules/react-native-paper -name "*.d.ts" -path "*/Card/*" | head -5

Repository: Devasy/splitwiser

Length of output: 129


🏁 Script executed:

# Search web for react-native-paper v5.14.5 Card.Title API documentation
# Let's also check if there's any documentation in the repository itself
find . -name "*.md" -type f ! -path "*/node_modules/*" -exec grep -l "Card.Title" {} \;

Repository: Devasy/splitwiser

Length of output: 106


🏁 Script executed:

# Check if there are any warnings or errors in the build output or eslint config
find . -name ".eslintrc*" -o -name "eslint.config.*" | xargs cat 2>/dev/null

Repository: Devasy/splitwiser

Length of output: 43


🌐 Web query:

react-native-paper v5.14.5 Card.Title title prop React element

💡 Result:

In react-native-paper v5.14.5, Card.Title’s title prop is typed as React.ReactNode, but it’s rendered inside a Text component, so in practice it only accepts a string or a <Text>-based node (i.e., content that can legally be nested inside <Text>). Passing something that renders a <View> (e.g., most icons wrapped in a <View>, or any layout container) will trigger the React Native error about nesting <View> inside <Text>. [1][2]

What you can do instead

  • Keep title as a string / <Text> node, and use left / right for icons/actions. [1]
  • If you need a fully custom title layout (icon + text with exact spacing), don’t use Card.Title’s title; build your own header row with Card.Content (or a custom View) and a Text + icon arrangement. (This avoids the <Text> nesting limitation.) [2]

Sources: React Native Paper Card.Title docs [1] and discussion of the practical limitation/error [2].


Remove the Skeleton component from Card.Title's title prop—it will cause a runtime error.

In react-native-paper v5.14.5, Card.Title's title prop is rendered inside a <Text> component. Since Skeleton returns an Animated.View, nesting it inside Text violates React Native's layout rules and will crash.

Fix: Use a string for title and move the skeleton to the left prop (already correct) or right prop, or restructure without Card.Title if you need full layout control:

Example fix: Move title skeleton to Card.Content
<Card key={key} style={styles.card}>
  <Card.Title
    title="Group Name"
    left={(props) => (
      <Skeleton width={40} height={40} borderRadius={20} style={props.style} />
    )}
  />
  <Card.Content>
    <Skeleton width={120} height={20} />
    <Skeleton width={200} height={16} style={{ marginTop: 4 }} />
  </Card.Content>
</Card>
🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@mobile/components/skeletons/GroupListSkeleton.js` around lines 14 - 20,
Card.Title's title currently receives a Skeleton component which will crash
because Skeleton renders an Animated.View that cannot be nested inside Text;
update the Card rendering (in GroupListSkeleton.js) so Card.Title.title is a
plain string (e.g., "Group Name") and move the visual skeletons into
Card.Content or into the left/right props (left already uses Skeleton) instead;
ensure no Skeleton component is passed directly to Card.Title.title or any prop
that is rendered inside a Text node.

<Card.Content>
<Skeleton width={200} height={16} style={{ marginTop: 4 }} />
</Card.Content>
</Card>
))}
</View>
);
};

const styles = StyleSheet.create({
list: {
padding: 16,
},
card: {
marginBottom: 16,
},
});

export default GroupListSkeleton;
46 changes: 46 additions & 0 deletions mobile/components/ui/Skeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { useEffect, useRef } from 'react';
import { Animated, StyleSheet, View } from 'react-native';
import { useTheme } from 'react-native-paper';

const Skeleton = ({ width, height, borderRadius, style }) => {
const theme = useTheme();
const opacity = useRef(new Animated.Value(0.3)).current;

useEffect(() => {
const animation = Animated.loop(
Animated.sequence([
Animated.timing(opacity, {
toValue: 0.7,
duration: 800,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 0.3,
duration: 800,
useNativeDriver: true,
}),
])
);

animation.start();

return () => animation.stop();
}, [opacity]);

return (
<Animated.View
style={[
{
opacity,
backgroundColor: theme.colors.surfaceVariant,
width,
height,
borderRadius: borderRadius || 4,
},
style,
]}
/>
);
};

export default Skeleton;
5 changes: 2 additions & 3 deletions mobile/screens/HomeScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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);
Expand Down Expand Up @@ -257,9 +258,7 @@ const HomeScreen = ({ navigation }) => {
</Appbar.Header>

{isLoading ? (
<View style={styles.loaderContainer}>
<ActivityIndicator size="large" />
</View>
<GroupListSkeleton />
) : (
<FlatList
data={groups}
Expand Down
Loading