Skip to content

Conversation

@Devasy23
Copy link
Owner

@Devasy23 Devasy23 commented Aug 14, 2025

Enhance the user interface of the Home and Join Group screens by introducing animated cards, gradient components, and a cohesive theme. Implement new animation utilities for smoother transitions and interactions, improving overall visual appeal and user experience.

Summary by CodeRabbit

  • New Features

    • Global toast system with hooks, haptic helpers, and a centralized design system.
  • Components

    • New animated & gradient UI primitives, cards, skeletons, empty states, inputs, buttons, icons, and modern navigation components; new Add Expense and Group Details screens.
  • UI/UX Improvements

    • Large visual overhaul across Home, Group Details, Group Settings, Join Group with glassy gradients, animations, loaders, and improved flows.
  • Dependencies

    • Added expo-haptics and expo-linear-gradient.
  • Chores

    • Preview workflow now supports manual triggering from GitHub.

- Enhanced HomeScreen with animated cards, improved layout, and gradient status indicators.
- Updated JoinGroupScreen to include a gradient card and refined input fields.
- Introduced new animation utilities for smoother transitions and interactions.
- Added gradient components for better visual appeal.
- Implemented a cohesive theme with updated colors, spacing, and typography.
@Devasy23 Devasy23 requested a review from vrajpatelll as a code owner August 14, 2025 19:39
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 14, 2025

Walkthrough

Adds a manual CI trigger and a large frontend design-system and UI overhaul: theme, gradients, animations, haptics, cards, inputs, skeletons, empty-states, icons, toasts, navigation components, modern screens, and provider wiring (Paper theme + ToastProvider). Two Expo packages added.

Changes

Cohort / File(s) Summary
CI Workflow
.github/workflows/preview.yml
Add workflow_dispatch to enable manual workflow runs alongside pull_request_target.
App Initialization & Providers
frontend/App.js
Apply paperTheme to PaperProvider and wrap AppNavigator with ToastProvider (Auth -> Paper(theme) -> Toast -> Navigator).
Dependencies
frontend/package.json
Add expo-haptics ^14.1.4 and expo-linear-gradient ^14.1.5.
Design Tokens & Theme
frontend/utils/theme.js
Add centralized design system: colors, tokens, spacing, borderRadius, typography, shadows, animations, glassmorphism, paperTheme, and default theme export.
Animations Primitives
frontend/utils/animations.js
Add AnimatedCard, SlideInView, FadeInView, ScaleInView, PulseView.
Gradients & Status Wrappers
frontend/utils/gradients.js
Add GradientBackground, GradientCard, GradientTextBackground, StatusGradient with status→gradient mappings.
Haptics
frontend/utils/haptics.js
Add hapticFeedback wrapper around expo-haptics (light/medium/heavy/success/warning/error/selection).
Cards & Quick Actions
frontend/utils/cards.js
Add GlassCard, ExpenseCard, GroupSummaryCard, legacy GroupCard, QuickActionCard (gradients, haptics, formatting).
Inputs / Core Input Components
frontend/utils/inputs.js, frontend/components/core/Input.js
Add EnhancedTextInput, CurrencyInput, SearchInput with focus/validation, secure toggle, currency formatting, and haptics.
Icons
frontend/utils/icons.js
Add ThemedIconButton, ActionButton, StatusIcon, CurrencyIcon, FeatureIcon.
Toast System
frontend/utils/toast.js
Add TOAST_TYPES, TOAST_MESSAGES, ToastProvider, and useToast hook; Snackbar-based toasts with type-based haptics and usage guard.
Skeletons
frontend/utils/skeletons.js
Add ShimmerEffect, GroupCardSkeleton, ExpenseItemSkeleton, MemberItemSkeleton, SkeletonList.
Empty States
frontend/utils/emptyStates.js
Add EmptyState and specialized variants: EmptyGroups, EmptyExpenses, EmptyFriends, EmptySearch, AllSettled.
Cards UI Library & Buttons
frontend/components/core/Button.js
Add Button (default) / ModernButton alias with variants, sizes, gradients, loading, and haptics.
Navigation Components
frontend/components/navigation/ModernNavigation.js
Add ModernHeader, ModernTabBar, ActionButton, FloatingActionButton (modern tab bar and header primitives).
Screens — Modern Group & Expense Flows
frontend/screens/ModernGroupDetailsScreen.js, frontend/screens/ModernAddExpenseScreen.js
Add modern group detail and add-expense screens (animated, gradient UIs, settlement calculations, split methods, member selection).
Screens — Rewrites / Themed UIs
frontend/screens/HomeScreen.js, frontend/screens/GroupDetailsScreen.js, frontend/screens/GroupSettingsScreen.js, frontend/screens/JoinGroupScreen.js
Major UI refactors to glass/gradient/animated designs; preserve data interactions while changing presentation and loading/empty states.
Navigation — Routes & Tab Bar
frontend/navigation/GroupsStackNavigator.js, frontend/navigation/MainNavigator.js
Replace legacy screens with modern variants in Groups stack; swap built-in tab bar for ModernTabBar and add tab labels.
Utilities — Cards/Blueprint Doc
frontend/A Design Blueprint for a Modern, Minimalist Expens.md
Add design blueprint document describing tokens, components, motion, and rollout guidance.

Sequence Diagram(s)

sequenceDiagram
  participant C as Component
  participant TP as ToastProvider
  participant HF as Haptics
  participant SN as Snackbar

  C->>TP: useToast().showToast(type, message, duration)
  TP->>HF: trigger haptic by type
  TP->>SN: set visible {message, type, duration}
  SN-->>TP: onDismiss (timeout / user)
  TP->>TP: clear toast state
Loading
sequenceDiagram
  participant GS as GroupSettingsScreen
  participant SET as getOptimizedSettlements
  participant UI as Alert/Confirm
  participant API as backend

  GS->>SET: check unsettled balances for memberId
  SET-->>GS: {isSettled, details}
  alt has unsettled balances
    GS->>UI: show alert "member has unsettled balances" (abort)
  else settled
    GS->>UI: show confirm "remove member?"
    UI-->>GS: confirmed
    GS->>API: removeMember(memberId)
    API-->>GS: success
    GS->>GS: refresh members
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement, frontend, expo, level3, groups

Suggested reviewers

  • vrajpatelll

Poem

🥕 I hopped through tokens, gradients bright,
I nudged the cards to slide in light.
Toasts that buzz and buttons that glow,
Haptics tap where microflows go.
A little rabbit shipped the show.

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/ui-changes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@codecov
Copy link

codecov bot commented Aug 14, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.79%. Comparing base (2aacca8) to head (5ffd608).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #147   +/-   ##
=======================================
  Coverage   72.79%   72.79%           
=======================================
  Files          17       17           
  Lines        1669     1669           
  Branches      154      154           
=======================================
  Hits         1215     1215           
  Misses        400      400           
  Partials       54       54           
Components Coverage Δ
Authentication System 75.45% <ø> (ø)
Expense Management 69.88% <ø> (ø)
Group Management 69.49% <ø> (ø)
User Management 97.16% <ø> (ø)
Backend Core 69.41% <ø> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Devasy23 Devasy23 added the run-preview To run Expo preview build on forks label Aug 14, 2025
@Devasy23 Devasy23 self-assigned this Aug 14, 2025
- Implemented ToastProvider for global toast notifications with different types (success, error, warning, info).
- Added haptic feedback utilities for various interaction types (light, medium, heavy, success, warning, error).
- Enhanced App.js to include ToastProvider for global access to toast notifications.
- Created EnhancedTextInput component with haptic feedback on focus and secure entry toggle.
- Developed GroupCard, ExpenseCard, MemberCard, and SummaryCard components with haptic feedback on interactions.
- Introduced skeleton loading components for GroupCard, ExpenseItem, and MemberItem.
- Added empty state components for groups, expenses, and friends with action prompts.
- Created themed icon buttons and action buttons with consistent styling.
- Implemented currency input with automatic formatting and search input with clear button functionality.
@Devasy23 Devasy23 requested review from jinang17 and monkscode August 14, 2025 19:47
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 19

🔭 Outside diff range comments (8)
.github/workflows/preview.yml (4)

13-13: Job will never run on workflow_dispatch due to PR-only condition

The if checks PR labels, which are undefined in workflow_dispatch events, so the preview job is skipped for manual runs.

Fix the condition to support both triggers and keep the label gate for PRs:

-    if: contains(github.event.pull_request.labels.*.name, 'run-preview')
+    if: |
+      (github.event_name == 'pull_request_target' &&
+       contains(github.event.pull_request.labels.*.name, 'run-preview')) ||
+      (github.event_name == 'workflow_dispatch')

Optional hardening to ensure only maintainers trigger runs with secrets:

+      && (github.event.sender.association == 'OWNER' ||
+          github.event.sender.association == 'MEMBER' ||
+          github.event.sender.association == 'COLLABORATOR')

17-20: Checkout ref is undefined for workflow_dispatch

github.event.pull_request.head.sha doesn’t exist for workflow_dispatch, causing checkout to fail.

Use the workflow input for manual runs, with sane fallbacks:

       - name: Checkout repository
         uses: actions/checkout@v5
         with:
-          ref: ${{ github.event.pull_request.head.sha }}
+          ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.ref }}

39-40: Expo branch argument depends on PR context; add fallbacks for manual runs

github.event.pull_request.head.ref is undefined on workflow_dispatch. Provide a fallback to the manual input or current ref name.

-          command: eas update --auto --branch ${{ github.event.pull_request.head.ref }}
+          command: eas update --auto --branch ${{ github.event.pull_request.head.ref || inputs.ref || github.ref_name }}

27-31: High risk: pull_request_target + checkout of PR HEAD + secrets (EXPO_TOKEN) — tighten immediately

This workflow checks out untrusted PR code while exposing EXPO_TOKEN; the label gate exists but does not remove the exfiltration risk.

  • Findings (from repo scan):

    • .github/workflows/preview.yml — on: pull_request_target (line 4)
    • .github/workflows/preview.yml — job-level if: contains(github.event.pull_request.labels.*.name, 'run-preview') (line 13) — label gate present
    • .github/workflows/preview.yml — actions/checkout@v5 with ref: ${{ github.event.pull_request.head.sha }} (lines 17–19) — checks out PR HEAD
    • .github/workflows/preview.yml — expo/expo-github-action@v8 using token: ${{ secrets.EXPO_TOKEN }} (lines 27–31)
    • .github/workflows/preview.yml — expo/expo-github-action/preview@v8 running eas update against the PR branch (lines 37–41)
  • Vulnerable snippet (unchanged):

          uses: expo/expo-github-action@v8
          with:
            eas-version: latest
            token: ${{ secrets.EXPO_TOKEN }}
    
  • Recommended remediations (pick one or combine):

    1. Do NOT run untrusted PR code with repo secrets. Change this job to run on pull_request (not pull_request_target), or remove checkout of the PR head in pull_request_target handlers and avoid using secrets there.
    2. Preferred: split into two jobs — Job A: pull_request build/test (no secrets) → upload artifacts; Job B: maintainer-gated job (or workflow_run triggered by a maintainer/label) that consumes artifacts and uses EXPO_TOKEN (no checkout of PR HEAD).
    3. If keeping the label gate, add a maintainer check (ensure the label was applied by a repo collaborator/maintainer via author_association or an approval step) before exposing secrets.
  • Also audit other workflows that use EXPO_TOKEN (bundle-analysis.yml, rn-bundle-analysis.yml, publish.yml) to ensure none combine pull_request_target + checkout of PR HEAD.

Do you want me to draft the two-job pattern with artifact handoff (example workflow) to harden this?

frontend/screens/JoinGroupScreen.js (1)

16-36: Harden input validation and callback invocation; normalize code before API call

  • Validate trimmed length (6–8 chars).
  • Normalize to uppercase before calling joinGroup.
  • Guard onGroupJoined in case route.params is missing.
   const handleJoinGroup = async () => {
-    if (!joinCode) {
-      Alert.alert("Error", "Please enter a join code.");
+    const normalized = joinCode.trim().toUpperCase();
+    if (!normalized || normalized.length < 6) {
+      Alert.alert("Error", "Please enter a valid 6–8 character join code.");
       return;
     }
     setIsJoining(true);
     try {
-      await joinGroup(joinCode);
+      await joinGroup(normalized);
       Alert.alert("Success", "Successfully joined the group! 🎉");
-      onGroupJoined(); // Call the callback to refresh the groups list
+      onGroupJoined?.(); // refresh the groups list if provided
       navigation.goBack();

Optionally, add a safe default at declaration:

// outside this block:
const onGroupJoined = route?.params?.onGroupJoined ?? (() => {});
frontend/screens/GroupDetailsScreen.js (1)

29-34: Reuse shared currency utils instead of local formatter

There’s a local currency symbol and formatter; elsewhere you already use centralized currency utilities. This duplication risks inconsistent symbols/formatting and weakens validation.

Apply these diffs:

-import { AnimatedCard, FadeInView, ScaleInView, SlideInView } from "../utils/animations";
+import { AnimatedCard, FadeInView, ScaleInView, SlideInView } from "../utils/animations";
+import { formatCurrency } from "../utils/currency";
-// Currency configuration - can be made configurable later
-const currency = "₹"; // Default to INR, can be changed to '$' for USD
-
-// Helper function to format currency amounts
-const formatCurrency = (amount) => `${currency}${amount.toFixed(2)}`;
+// Currency formatting via shared utility

Note: The imported formatCurrency returns a symbol-prefixed, validated string and aligns with app-wide settings.

Also applies to: 15-17

frontend/screens/GroupSettingsScreen.js (2)

137-147: Carry through mimeType from the picker and clear emoji selection when an image is chosen

The picker returns asset.mimeType (or asset.type on some SDKs). Store it so onSave can build a correct data URL. Also, clear any previously selected emoji to avoid ambiguity about which icon will be saved.

Apply:

   const result = await ImagePicker.launchImageLibraryAsync({
     mediaTypes: ImagePicker.MediaTypeOptions.Images,
     base64: true,
     allowsEditing: true,
     aspect: [1, 1],
     quality: 0.8,
   });
   if (!result.canceled && result.assets && result.assets.length > 0) {
     const asset = result.assets[0];
-    setPickedImage({ uri: asset.uri, base64: asset.base64 });
+    const mimeType =
+      asset.mimeType ||
+      asset.type ||
+      (asset.uri?.toLowerCase().endsWith(".png") ? "image/png" : "image/jpeg");
+    setPickedImage({ uri: asset.uri, base64: asset.base64, mimeType });
+    // Ensure image selection takes precedence over emoji choice
+    setIcon("");
   }

92-106: Fix incorrect data URL MIME type for uploaded group images — backend has no icon field, so apply MIME fix only

The app currently hardcodes data:image/jpeg;base64,... for picked images (can be PNG/WebP/GIF). Backend/group API only supports imageUrl (no icon field), so don't change storage of emoji to a separate icon field yet — just fix MIME handling and capture mimeType on pick.

Files to update

  • frontend/screens/GroupSettingsScreen.js
    • onSave (current imageUrl assignment) — ~lines 92–106
    • pickImage (setPickedImage usage) — ~line 126+
  • Reference implementation: frontend/screens/EditProfileScreen.js (mime detection + setPickedImage) — use as copy/paste reference (lines ~26, ~64–79).
  • Evidence backend has no icon field:
    • ui-poc/openapi.json — GroupUpdateRequest only lists imageUrl
    • backend/app/groups/schemas.py and backend/app/groups/service.py reference only imageUrl

Suggested diffs (apply MIME fix and capture mimeType when picking):

Replace image assignment in onSave:

-    if (pickedImage?.base64) {
-      // If user picked an image, use it as imageUrl
-      updates.imageUrl = `data:image/jpeg;base64,${pickedImage.base64}`;
+    if (pickedImage?.base64) {
+      // If user picked an image, use it as imageUrl (preserve actual MIME)
+      const mime =
+        pickedImage.mimeType && /image\//.test(pickedImage.mimeType)
+          ? pickedImage.mimeType
+          : "image/jpeg";
+      updates.imageUrl = `data:${mime};base64,${pickedImage.base64}`;
     } else if (icon && icon !== (group?.imageUrl || group?.icon || "")) {
       // If user selected an emoji and it's different from current
       // Check if it's an emoji (not a URL)
       const isEmoji = ICON_CHOICES.includes(icon);
-      if (isEmoji) {
-        updates.imageUrl = icon; // Store emoji as imageUrl for now
-      } else {
-        updates.imageUrl = icon; // Store other text/URL as imageUrl
-      }
+      if (isEmoji) {
+        // Keep emoji in imageUrl for now — backend doesn't expose an `icon` field
+        updates.imageUrl = icon;
+      } else {
+        updates.imageUrl = icon; // Store other text/URL as imageUrl
+      }
     }

Capture mimeType in pickImage (mirror EditProfileScreen.js):

-    if (!result.canceled && result.assets && result.assets.length > 0) {
-      const asset = result.assets[0];
-      setPickedImage({ uri: asset.uri, base64: asset.base64 });
-    }
+    if (!result.canceled && result.assets && result.assets.length > 0) {
+      const asset = result.assets[0];
+      // Derive mimeType (expo may provide mimeType on newer SDKs)
+      let mimeType = asset.mimeType || asset.type;
+      if (mimeType && !/image\//.test(mimeType)) {
+        if (mimeType === "image") mimeType = "image/jpeg";
+      }
+      if (!mimeType || !/image\//.test(mimeType)) {
+        const ext = asset.uri?.split(".").pop()?.toLowerCase();
+        if (ext === "png") mimeType = "image/png";
+        else if (ext === "webp") mimeType = "image/webp";
+        else if (ext === "gif") mimeType = "image/gif";
+        else if (ext === "jpg" || ext === "jpeg") mimeType = "image/jpeg";
+        else mimeType = "image/jpeg";
+      }
+      setPickedImage({ uri: asset.uri, base64: asset.base64, mimeType });
+    }

Notes

  • I verified backend OpenAPI and schemas show GroupUpdateRequest only accepts imageUrl (no icon). If the backend later adds a dedicated icon field, move emoji into updates.icon and keep imageUrl reserved for image URIs/data-URLs.
  • Use frontend/screens/EditProfileScreen.js as exact reference for mime heuristics to keep behavior consistent.
🧹 Nitpick comments (36)
.github/workflows/preview.yml (3)

12-15: Add concurrency to avoid duplicate previews

Avoid wasting EAS updates on multiple in-flight runs for the same PR/branch.

   preview:
+    concurrency:
+      group: preview-${{ github.event.pull_request.number || inputs.ref || github.ref_name }}
+      cancel-in-progress: true

32-34: Prefer npm ci in CI for reproducibility and speed

npm ci is faster and honors lockfiles; npm install can mutate package-lock.json.

-      - name: Install dependencies
-        run: npm install
+      - name: Install dependencies
+        run: npm ci

17-17: Pin action versions to commit SHAs for supply chain hardening

Pinning to a major tag can be mutable. Consider pinning to a full commit SHA (with a comment noting the version) for checkout, setup-node, and expo actions.

Example:

-        uses: actions/checkout@v5
+        uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v5

-        uses: actions/setup-node@v4
+        uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4

-        uses: expo/expo-github-action@v8
+        uses: expo/expo-github-action@c6c6bf1393f7ec7f7364edf1f3f1a83b32f68f6b # v8

-        uses: expo/expo-github-action/preview@v8
+        uses: expo/expo-github-action/preview@c6c6bf1393f7ec7f7364edf1f3f1a83b32f68f6b # v8

(Replace SHAs with the exact commits you intend to trust.)

Also applies to: 22-22, 27-27, 37-37

frontend/utils/haptics.js (1)

1-39: Optional: guard or swallow errors on unsupported platforms

Expo Haptics is no-op on some platforms/simulators, but defensive error swallowing avoids console noise. Implement a small helper to wrap calls if desired.

+const safe = (p) => {
+  // Explicitly swallow rejections in unsupported environments
+  if (p?.catch) p.catch(() => {});
+  return p;
+};
...
-  light: () => {
-    return Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
-  },
+  light: () => safe(Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)),
...
-  selection: () => {
-    return Haptics.selectionAsync();
-  },
+  selection: () => safe(Haptics.selectionAsync()),
frontend/utils/theme.js (1)

174-194: Paper theme completeness: consider MD3 base theme merge to inherit defaults

This theme defines a subset of MD3 color roles. Merging with MD3LightTheme ensures future-proof defaults without repeating the entire palette.

For example, in App.js:

import { MD3LightTheme, Provider as PaperProvider } from 'react-native-paper';
import { paperTheme } from './utils/theme';

const theme = {
  ...MD3LightTheme,
  colors: { ...MD3LightTheme.colors, ...paperTheme.colors },
  roundness: paperTheme.roundness,
};

No code changes needed in this file; sharing for consistency.

frontend/utils/skeletons.js (2)

9-28: Stop the animation and clear any queued frames on unmount

You already stop the loop, which is good. To be extra safe and avoid any residual scheduled frames in rapid mounts/unmounts, also stop the underlying Animated.Value.

   useEffect(() => {
     const shimmerAnimation = Animated.loop(
       Animated.sequence([
         Animated.timing(shimmerValue, {
           toValue: 1,
           duration: 1000,
           useNativeDriver: true,
         }),
         Animated.timing(shimmerValue, {
           toValue: 0,
           duration: 1000,
           useNativeDriver: true,
         }),
       ])
     );

     shimmerAnimation.start();

-    return () => shimmerAnimation.stop();
+    return () => {
+      shimmerAnimation.stop();
+      shimmerValue.stopAnimation();
+    };
   }, [shimmerValue]);

37-41: Prevent skeletons from intercepting touches

Skeletons are purely visual; avoid capturing touches by disabling pointer events.

-  return (
-    <Animated.View style={[style, shimmerStyle]}>
+  return (
+    <Animated.View style={[style, shimmerStyle]} pointerEvents="none">
       {children}
     </Animated.View>
   );
frontend/utils/icons.js (3)

156-164: CurrencyIcon: same non-interactive concern; disable and set appropriate a11y role

Aligns semantics with intent and avoids accidental presses.

   return (
     <IconButton
       icon={getCurrencyIcon()}
       size={size}
       iconColor={colors.primary}
       style={styles.currencyIcon}
+      disabled
+      accessibilityRole="image"
+      accessibilityLabel={`${currency} currency`}
     />
   );

66-107: ActionButton sizing logic is good; ensure hitSlop for smaller sizes

For 40x40 buttons, consider adding hitSlop for better accessibility and usability, especially on dense lists.

     <ThemedIconButton
       icon={icon}
       size={sizeStyle.iconSize}
       onPress={onPress}
       variant={variant}
       disabled={disabled}
+      hitSlop={{ top: 8, right: 8, bottom: 8, left: 8 }}
       style={[
         {
           width: sizeStyle.width,
           height: sizeStyle.height,
           borderRadius: sizeStyle.width / 2,
         },
         style,
       ]}
       {...props}
     />

6-43: Variant map is clear; consider consolidating to reduce duplication

A small map object reduces switch verbosity and makes extension simpler.

-  const getIconStyle = () => {
-    switch (variant) {
-      case 'primary':
-        return {
-          backgroundColor: colors.primaryLight,
-          iconColor: colors.primary,
-        };
-      case 'success':
-        return {
-          backgroundColor: colors.successLight,
-          iconColor: colors.success,
-        };
-      case 'warning':
-        return {
-          backgroundColor: colors.warningLight,
-          iconColor: colors.warning,
-        };
-      case 'error':
-        return {
-          backgroundColor: colors.errorLight,
-          iconColor: colors.error,
-        };
-      default:
-        return {
-          backgroundColor: colors.surfaceVariant,
-          iconColor: colors.onSurfaceVariant,
-        };
-    }
-  };
+  const variants = {
+    primary: { backgroundColor: colors.primaryLight, iconColor: colors.primary },
+    success: { backgroundColor: colors.successLight, iconColor: colors.success },
+    warning: { backgroundColor: colors.warningLight, iconColor: colors.warning },
+    error:   { backgroundColor: colors.errorLight, iconColor: colors.error },
+    default: { backgroundColor: colors.surfaceVariant, iconColor: colors.onSurfaceVariant },
+  };
+  const getIconStyle = () => variants[variant] ?? variants.default;
frontend/utils/animations.js (2)

49-62: Prefer Pressable for better accessibility; add role/disabled props

TouchableWithoutFeedback provides no visual feedback or accessibility semantics. Pressable offers richer press states and accessibility by default. At minimum, add accessibilityRole; ideally, switch to Pressable and pass disabled through.

Apply this diff to switch to Pressable and improve accessibility:

-  if (onPress && !disabled) {
+  if (onPress && !disabled) {
     return (
-      <TouchableWithoutFeedback
-        onPressIn={handlePressIn}
-        onPressOut={handlePressOut}
-        onPress={onPress}
-        {...props}
-      >
+      <Pressable
+        accessibilityRole="button"
+        accessibilityState={{ disabled: !!disabled }}
+        onPressIn={handlePressIn}
+        onPressOut={handlePressOut}
+        onPress={onPress}
+        disabled={disabled}
+        {...props}
+      >
         <Animated.View style={[style, animatedStyle]}>
           {children}
         </Animated.View>
-      </TouchableWithoutFeedback>
+      </Pressable>
     );
   }

Additionally update the imports:

-import { Animated, TouchableWithoutFeedback, View } from 'react-native';
+import { Animated, Pressable, View } from 'react-native';

76-93: Stop animations on unmount to prevent leaks and jitter on rapid remounts

The setTimeout handle is cleared, but any started animations continue. Keep a reference and stop animations in the cleanup. This helps when navigating quickly between screens or unmounting mid-animation.

Example patch for SlideInView:

   useEffect(() => {
     const timer = setTimeout(() => {
-      Animated.parallel([
+      const animation = Animated.parallel([
         Animated.timing(translateY, {
           toValue: 0,
           duration: animations.timing.medium,
           useNativeDriver: true,
         }),
         Animated.timing(opacity, {
           toValue: 1,
           duration: animations.timing.medium,
           useNativeDriver: true,
         }),
-      ]).start();
+      ]);
+      animation.start();
+      // keep a ref if you want to stop later
+      animation.__ref = true;
+      SlideInView.__lastAnimation = animation;
     }, delay);

-    return () => clearTimeout(timer);
+    return () => {
+      clearTimeout(timer);
+      // stop if it already started
+      SlideInView.__lastAnimation?.stop?.();
+    };
   }, [delay, translateY, opacity]);

Apply the same pattern to FadeInView and ScaleInView to stop running animations in their cleanup functions.

Also applies to: 114-125, 137-149

frontend/utils/gradients.js (1)

119-124: Drop the default export object to avoid ambiguous import patterns

Providing both named exports and a default object that re-exports them commonly causes import confusion and hinders tree-shaking. Prefer named exports only.

Apply this diff:

-export default {
-  GradientBackground,
-  GradientCard,
-  GradientTextBackground,
-  StatusGradient,
-};
+// Prefer named exports only; remove default export for clarity and tree-shaking.
frontend/utils/cards.js (2)

145-147: Format expense amount

-          <Text style={styles.expenseAmount}>{expense.amount}</Text>
+          <Text style={styles.expenseAmount}>{formatCurrency(expense.amount)}</Text>

231-238: Improve accessibility for destructive “Remove” action

Add accessibilityRole and an explicit label to aid screen readers.

         <TouchableOpacity
           style={styles.removeButton}
           onPress={handleRemove}
+          accessibilityRole="button"
+          accessibilityLabel={`Remove ${member?.user?.name || 'member'}`}
         >
frontend/screens/JoinGroupScreen.js (1)

88-101: Disable CTA until the join code is plausibly valid

Currently disabled only when empty. Align with 6–8 char guidance.

             <Button
               mode="contained"
               onPress={handleJoinGroup}
               loading={isJoining}
-              disabled={isJoining || !joinCode.trim()}
+              disabled={isJoining || joinCode.trim().length < 6}
               style={[
                 styles.joinButton,
-                { opacity: joinCode.trim() ? 1 : 0.6 }
+                { opacity: joinCode.trim().length >= 6 ? 1 : 0.6 }
               ]}
               contentStyle={styles.buttonContent}
               labelStyle={styles.buttonLabel}
             >
frontend/utils/emptyStates.js (2)

33-40: Use Paper’s buttonColor prop instead of styling backgroundColor

Styling backgroundColor on Paper Button can conflict with theme and disabled/pressed states. Prefer buttonColor prop and keep styles for layout/radius only.

Apply these diffs:

       <Button
         mode="contained"
         onPress={onAction}
-        style={styles.emptyAction}
+        style={styles.emptyAction}
+        buttonColor={colors.primary}
         contentStyle={styles.emptyActionContent}
       >
   emptyAction: {
-    backgroundColor: colors.primary,
     borderRadius: borderRadius.lg,
   },

Also applies to: 121-124


76-82: Quote handling in search subtitle

If searchTerm contains quotes/newlines, the inline quoted sentence can break visually. Consider trimming and truncating long terms for readability.

Example:

-    subtitle={`We couldn't find anything matching "${searchTerm}". Try adjusting your search.`}
+    subtitle={`We couldn't find anything matching "${String(searchTerm ?? '').trim().slice(0, 50)}". Try adjusting your search.`}
frontend/screens/GroupDetailsScreen.js (5)

19-19: Remove unused Dimensions constant

width is declared but never used.

-const { width } = Dimensions.get('window');

71-74: Defensive member name lookup

If members doesn’t contain the entry or shapes vary, this can throw when accessing member.user.name. Use optional chaining and a safe fallback.

-  const getMemberName = (userId) => {
-    const member = members.find((m) => m.userId === userId);
-    return member ? member.user.name : "Unknown";
-  };
+  const getMemberName = (userId) => {
+    const member = members.find((m) => m.userId === userId);
+    return member?.user?.name ?? "Unknown";
+  };

85-94: Consistency: prefer “You owe” over “You borrowed”

Elsewhere (HomeScreen) you use “You owe”. Unify phrasing for a consistent UX copy across screens.

-      balanceText = `💳 You borrowed ${formatCurrency(Math.abs(net))}`;
+      balanceText = `💳 You owe ${formatCurrency(Math.abs(net))}`;

239-246: Reuse new EmptyExpenses component for consistency and less duplication

You introduced frontend/utils/emptyStates.js with EmptyExpenses. Use it here to keep styling, copy, and animation consistent (and drop duplicated styles).

Apply these diffs:

-import { GradientCard, StatusGradient } from "../utils/gradients";
+import { GradientCard, StatusGradient } from "../utils/gradients";
+import { EmptyExpenses } from "../utils/emptyStates";
-        ListEmptyComponent={
-          <FadeInView style={styles.emptyContainer}>
-            <Text style={styles.emptyTitle}>No expenses yet! 💸</Text>
-            <Text style={styles.emptyText}>
-              Add your first expense to start tracking group spending
-            </Text>
-          </FadeInView>
-        }
+        ListEmptyComponent={
+          <EmptyExpenses
+            onAddExpense={() => navigation.navigate("AddExpense", { groupId })}
+            groupName={groupName}
+          />
+        }

Optionally remove now-unused styles: emptyContainer, emptyTitle, emptyText.

Also applies to: 15-17


56-66: Navigation options in effect: include deps or use useLayoutEffect

You call navigation.setOptions inside a useEffect with [token, groupId]. To avoid stale closures and align with React Navigation’s guidance, include navigation and groupName in deps or switch to useLayoutEffect.

-  useEffect(() => {
+  // useLayoutEffect ensures header updates occur during layout phase
+  useLayoutEffect(() => {
     navigation.setOptions({
       title: groupName,
       headerRight: () => (
         <IconButton
           icon="cog"
           onPress={() => navigation.navigate("GroupSettings", { groupId })}
         />
       ),
     });
     if (token && groupId) {
       fetchData();
     }
-  }, [token, groupId]);
+  }, [navigation, groupName, token, groupId]);

Make sure to import useLayoutEffect from React.

Also applies to: 69-69

frontend/screens/HomeScreen.js (3)

21-21: Remove unused Dimensions constant

width is declared but not used.

-const { width } = Dimensions.get('window');

291-305: Leverage EmptyGroups for consistent empty state and less code

You’ve added a reusable empty state module. Use EmptyGroups instead of duplicating UI here.

Apply these diffs:

-import { StatusGradient } from "../utils/gradients";
+import { StatusGradient } from "../utils/gradients";
+import { EmptyGroups } from "../utils/emptyStates";
-          ListEmptyComponent={
-            <FadeInView style={styles.emptyContainer}>
-              <Text style={styles.emptyTitle}>No groups yet! 🎯</Text>
-              <Text style={styles.emptyText}>
-                Create your first group to start splitting expenses with friends
-              </Text>
-              <Button
-                mode="contained"
-                onPress={showModal}
-                style={styles.createFirstGroupButton}
-                icon="plus"
-              >
-                Create Group
-              </Button>
-            </FadeInView>
-          }
+          ListEmptyComponent={<EmptyGroups onCreateGroup={showModal} />}

Optionally remove now-unused styles in this block.

Also applies to: 19-19


130-164: Nice: Staggered animated cards with status gradient

Per-item delay and StatusGradient status mapping improve perceived performance and clarity. Copy consistency with GroupDetails would make it even better.

frontend/utils/toast.js (2)

48-51: Keep toast state tidy after dismiss

You mark visible: false but keep the object around. Consider clearing state after dismissal to prevent stale renders and free resources.

  const hideToast = () => {
-    setToast(prev => prev ? { ...prev, visible: false } : null);
+    setToast(prev => (prev ? { ...prev, visible: false } : null));
+    // Optionally clear after a short delay to allow Snackbar animation to complete
+    setTimeout(() => setToast(null), 300);
  };

Adjust delay to match your Snackbar exit animation timing.

Also applies to: 81-81


96-101: Snackbar action.textColor is valid in react-native-paper v5 — confirm the package version

Short: react-native-paper v5 supports passing textColor on the Snackbar action (it maps to Button props), so the current code is fine if your project uses v5. If the project uses an older Paper version, remove textColor or apply the color via theme.

Files to check:

  • frontend/utils/toast.js (lines 96–101) — current snippet (no change needed on v5):
          action={{
            label: 'Dismiss',
            onPress: hideToast,
            textColor: toastStyle.textColor,
          }}
        >

Next step: verify react-native-paper version in package.json; if it's <5, either remove textColor or upgrade/explicitly theme the Snackbar.

frontend/utils/inputs.js (4)

76-87: Avoid nested touchables; prefer TextInput’s built-in right icon slot

Wrapping an IconButton with TouchableOpacity can complicate a11y and hit slop. Paper’s TextInput supports right/left props with TextInput.Icon for this.

Refactor to:

<TextInput
  mode="outlined"
  // ...
  left={leftIcon ? <TextInput.Icon icon={() => leftIcon} /> : undefined}
  right={
    secureTextEntry
      ? <TextInput.Icon icon={isSecure ? 'eye-off' : 'eye'} onPress={toggleSecureEntry} />
      : rightIcon ? <TextInput.Icon icon={() => rightIcon} onPress={onRightIconPress} /> : undefined
  }
/>

This removes the absolute-positioned wrappers and manual paddings.

Also applies to: 90-96


117-133: Currency formatter edge cases

The formatter allows leading '.' and multiple leading zeros; consider normalizing inputs for a smoother UX (e.g., convert '.' to '0.' and strip leading zeros before the decimal).

   const formatValue = (text) => {
     // Remove non-numeric characters except decimal point
     const numericValue = text.replace(/[^0-9.]/g, '');
     
     // Ensure only one decimal point
     const parts = numericValue.split('.');
+    if (numericValue === '.') return '0.';
+    if (parts[0].length > 1 && parts[0].startsWith('0')) {
+      parts[0] = String(Number(parts[0])); // normalize leading zeros
+    }
     if (parts.length > 2) {
       return parts[0] + '.' + parts.slice(1).join('');
     }
     
     // Limit decimal places to 2
     if (parts[1] && parts[1].length > 2) {
       return parts[0] + '.' + parts[1].substring(0, 2);
     }
     
     return numericValue;
   };

198-216: Manual icon padding/positioning complicates layout

Absolute-positioned containers and manual paddings (textInputWithLeftIcon, textInputWithRightIcon) are fragile across platforms/sizes. If you keep the current approach, at least set consistent hitSlop and accessibility props on wrappers.

Alternatively, prefer TextInput.Icon slots as suggested above to drop these styles entirely.

Also applies to: 218-228


156-168: Search clear button press target

Currently, the rightIcon is an IconButton without onPress and the wrapper handles press. To improve a11y, consider passing the handler to the icon itself and removing the wrapper or adding accessibilityRole="button" and accessibilityLabel="Clear search" to the wrapper.

Example:

-      rightIcon={
-        value ? (
-          <IconButton
-            icon="close"
-            iconColor={colors.onSurfaceVariant}
-            size={20}
-          />
-        ) : null
-      }
-      onRightIconPress={value ? handleClear : undefined}
+      rightIcon={
+        value ? (
+          <IconButton
+            icon="close"
+            iconColor={colors.onSurfaceVariant}
+            size={20}
+            onPress={handleClear}
+            accessibilityLabel="Clear search"
+          />
+        ) : null
+      }

Also applies to: 183-191

frontend/screens/GroupSettingsScreen.js (4)

303-309: Use containerColor for IconButton background and add an accessibility label

Paper’s IconButton supports containerColor. Setting background via style is brittle. Also add an a11y label.

             <IconButton
               icon="account-remove"
-              iconColor={colors.error}
+              iconColor="white"
+              containerColor={colors.error}
               onPress={() => onKick(m.userId, displayName)}
-              style={styles.removeButton}
+              accessibilityLabel={`Remove ${displayName}`}
             />

Optionally remove removeButton style if unused.


470-478: Improve accessibility for the Share button

Add an accessibility label for screen readers.

           <Button
             mode="contained"
             onPress={onShareInvite}
             icon="share-variant"
             style={styles.shareButton}
             contentStyle={styles.shareButtonContent}
             buttonColor="rgba(255, 255, 255, 0.2)"
             textColor="white"
+            accessibilityLabel="Share group invite code"
           >

88-91: Normalize and trim group name before diffing/saving

Prevents no-op updates due to whitespace and avoids persisting leading/trailing spaces.

-    const updates = {};
-    if (name && name !== group?.name) updates.name = name;
+    const updates = {};
+    const trimmedName = name.trim();
+    if (trimmedName && trimmedName !== group?.name) updates.name = trimmedName;

202-227: Optional: Pre-check balances before allowing “Leave group” for clearer UX

You already pre-check before kicking a member. Mirroring that here would give users an immediate, friendly message before the API call fails.

Do you want a snippet mirroring getOptimizedSettlements check here?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2aacca8 and afde178.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (17)
  • .github/workflows/preview.yml (1 hunks)
  • frontend/App.js (1 hunks)
  • frontend/package.json (1 hunks)
  • frontend/screens/GroupDetailsScreen.js (4 hunks)
  • frontend/screens/GroupSettingsScreen.js (3 hunks)
  • frontend/screens/HomeScreen.js (4 hunks)
  • frontend/screens/JoinGroupScreen.js (3 hunks)
  • frontend/utils/animations.js (1 hunks)
  • frontend/utils/cards.js (1 hunks)
  • frontend/utils/emptyStates.js (1 hunks)
  • frontend/utils/gradients.js (1 hunks)
  • frontend/utils/haptics.js (1 hunks)
  • frontend/utils/icons.js (1 hunks)
  • frontend/utils/inputs.js (1 hunks)
  • frontend/utils/skeletons.js (1 hunks)
  • frontend/utils/theme.js (1 hunks)
  • frontend/utils/toast.js (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (12)
frontend/utils/animations.js (1)
frontend/utils/theme.js (4)
  • animations (157-172)
  • animations (157-172)
  • colors (2-48)
  • colors (2-48)
frontend/utils/toast.js (2)
frontend/utils/haptics.js (2)
  • hapticFeedback (4-39)
  • hapticFeedback (4-39)
frontend/utils/emptyStates.js (1)
  • styles (94-151)
frontend/utils/skeletons.js (1)
frontend/screens/FriendsScreen.js (4)
  • View (174-191)
  • _ (200-202)
  • loop (155-172)
  • useContext (16-241)
frontend/utils/gradients.js (1)
frontend/utils/theme.js (4)
  • colors (2-48)
  • colors (2-48)
  • borderRadius (59-65)
  • borderRadius (59-65)
frontend/utils/cards.js (3)
frontend/utils/haptics.js (2)
  • hapticFeedback (4-39)
  • hapticFeedback (4-39)
frontend/utils/balanceCalculator.js (2)
  • member (70-70)
  • groupsWithDetails (167-198)
frontend/screens/FriendsScreen.js (1)
  • group (104-148)
frontend/utils/icons.js (1)
frontend/navigation/MainNavigator.js (3)
  • MaterialCommunityIcons (17-19)
  • MaterialCommunityIcons (35-37)
  • MaterialCommunityIcons (26-28)
frontend/screens/GroupDetailsScreen.js (5)
frontend/utils/currency.js (2)
  • formatCurrency (8-16)
  • formatCurrency (8-16)
frontend/utils/animations.js (8)
  • SlideInView (72-108)
  • SlideInView (72-108)
  • AnimatedCard (6-69)
  • AnimatedCard (6-69)
  • FadeInView (111-131)
  • FadeInView (111-131)
  • ScaleInView (134-162)
  • ScaleInView (134-162)
frontend/utils/gradients.js (5)
  • styles (104-117)
  • StatusGradient (69-102)
  • StatusGradient (69-102)
  • GradientCard (28-48)
  • GradientCard (28-48)
frontend/utils/balanceCalculator.js (1)
  • getMemberName (67-72)
frontend/utils/theme.js (10)
  • colors (2-48)
  • colors (2-48)
  • spacing (50-57)
  • spacing (50-57)
  • typography (67-122)
  • typography (67-122)
  • borderRadius (59-65)
  • borderRadius (59-65)
  • shadows (124-155)
  • shadows (124-155)
frontend/utils/inputs.js (2)
frontend/utils/haptics.js (2)
  • hapticFeedback (4-39)
  • hapticFeedback (4-39)
frontend/screens/GroupDetailsScreen.js (1)
  • currency (30-30)
frontend/screens/HomeScreen.js (6)
frontend/utils/currency.js (2)
  • formatCurrency (8-16)
  • formatCurrency (8-16)
frontend/utils/cards.js (2)
  • isImage (60-60)
  • groupIcon (61-61)
frontend/utils/animations.js (8)
  • SlideInView (72-108)
  • SlideInView (72-108)
  • AnimatedCard (6-69)
  • AnimatedCard (6-69)
  • FadeInView (111-131)
  • FadeInView (111-131)
  • ScaleInView (134-162)
  • ScaleInView (134-162)
frontend/utils/gradients.js (2)
  • StatusGradient (69-102)
  • StatusGradient (69-102)
frontend/utils/theme.js (10)
  • colors (2-48)
  • colors (2-48)
  • typography (67-122)
  • typography (67-122)
  • spacing (50-57)
  • spacing (50-57)
  • borderRadius (59-65)
  • borderRadius (59-65)
  • shadows (124-155)
  • shadows (124-155)
frontend/context/AuthContext.js (1)
  • isLoading (16-16)
frontend/screens/JoinGroupScreen.js (3)
frontend/utils/gradients.js (3)
  • styles (104-117)
  • GradientCard (28-48)
  • GradientCard (28-48)
frontend/utils/theme.js (10)
  • colors (2-48)
  • colors (2-48)
  • typography (67-122)
  • typography (67-122)
  • spacing (50-57)
  • spacing (50-57)
  • borderRadius (59-65)
  • borderRadius (59-65)
  • shadows (124-155)
  • shadows (124-155)
frontend/utils/animations.js (4)
  • FadeInView (111-131)
  • FadeInView (111-131)
  • ScaleInView (134-162)
  • ScaleInView (134-162)
frontend/App.js (5)
frontend/context/AuthContext.js (2)
  • AuthProvider (12-190)
  • AuthProvider (12-190)
frontend/utils/theme.js (2)
  • paperTheme (175-194)
  • paperTheme (175-194)
frontend/utils/toast.js (2)
  • ToastProvider (21-112)
  • ToastProvider (21-112)
frontend/navigation/AppNavigator.js (1)
  • AppNavigator (8-24)
frontend/navigation/AuthNavigator.js (1)
  • Stack (8-15)
frontend/screens/GroupSettingsScreen.js (7)
frontend/context/AuthContext.js (1)
  • user (13-13)
frontend/utils/animations.js (6)
  • SlideInView (72-108)
  • SlideInView (72-108)
  • FadeInView (111-131)
  • FadeInView (111-131)
  • AnimatedCard (6-69)
  • AnimatedCard (6-69)
frontend/screens/JoinGroupScreen.js (1)
  • styles (118-213)
frontend/utils/gradients.js (5)
  • styles (104-117)
  • GradientCard (28-48)
  • GradientCard (28-48)
  • StatusGradient (69-102)
  • StatusGradient (69-102)
frontend/screens/AccountScreen.js (1)
  • styles (67-85)
frontend/screens/AddExpenseScreen.js (1)
  • styles (453-492)
frontend/utils/theme.js (4)
  • colors (2-48)
  • colors (2-48)
  • spacing (50-57)
  • spacing (50-57)
🔇 Additional comments (11)
frontend/utils/skeletons.js (1)

82-88: Use stable keys when possible to reduce unnecessary remounts

Index keys are fine for static lists, but if count changes frequently it can cause re-mounts. Consider a stable array or explicit ids when used in dynamic loading screens.

Do these skeleton lists render with changing counts in your flows (e.g., switching tabs/filters)? If so, I can propose a small helper to memoize keys. Otherwise, feel free to keep as-is.

frontend/utils/gradients.js (1)

1-1: Dependency confirmed — no action needed

Confirmed: the import in frontend/utils/gradients.js is valid and expo-linear-gradient is declared and installed.

  • frontend/utils/gradients.js — import: import { LinearGradient } from 'expo-linear-gradient';
  • frontend/package.json (frontend/package.json:21) — "expo-linear-gradient": "^14.1.5"
  • frontend/package-lock.json / node_modules — lock entry for expo-linear-gradient version 14.1.5
frontend/App.js (1)

10-14: LGTM: Provider composition and theming integration are correct

PaperProvider wraps ToastProvider and AppNavigator, ensuring Snackbar/Toast gets theme context. AuthProvider stays outermost. This aligns with the new theme and toast system.

frontend/screens/JoinGroupScreen.js (1)

52-62: Nice hero section integration with gradients and fade-in

The gradient hero wrapped in FadeInView reads clean and aligns with the new design system.

frontend/utils/emptyStates.js (1)

7-43: Solid, reusable EmptyState abstraction

The component covers illustration, icon, title, subtitle, and optional CTA cleanly with entry animation. Good guard on rendering the action only when both actionText and onAction are present.

frontend/utils/toast.js (2)

24-39: Good haptics mapping per toast type

Triggering haptics per toast type is a UX win and the default to light on INFO is sensible.


115-131: Ergonomic hook API

The hook exposes convenience helpers showSuccess|Error|Warning|Info and enforces provider usage — clean and safe.

frontend/screens/GroupSettingsScreen.js (4)

266-313: Members list rendering looks solid

Good use of SlideInView per item, admin chip, and “You” labeling. The divider spacing and conditional admin-only remove action are clear.


318-345: Loading and header sections are clean and cohesive

Loader with FadeInView and the gradient header card are well-structured and align with the new design system.


455-482: Invite flow is straightforward and consistent

The StatusGradient usage, join code presentation, and Share integration fit the new UI pattern nicely.


602-605: gap is supported by RN 0.79 / Expo SDK 53 — no change required

Verified: frontend/package.json uses react-native 0.79.5 (Expo SDK ~53). React Native added gap in 0.71 (percentage support in 0.75+), so using gap here is supported.

Locations to note:

  • frontend/screens/GroupSettingsScreen.js — lines 602–605 (snippet shown)
  • Also applies to lines 627–630 and 776–778

Optional: only add the explicit-margin fallback if you need to support RN < 0.71.

Comment on lines 3 to 6
on:
pull_request_target:
workflow_dispatch:

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Manual dispatch added, but missing inputs and noisy triggers; define inputs and narrow pull_request_target types

Without inputs, workflow_dispatch runs won’t know which ref/PR to build, and the job may still trigger on many PR events unnecessarily. Define explicit inputs and restrict pull_request_target event types to when useful.

Apply:

 on:
   pull_request_target:
+    types: [labeled, synchronize, reopened]
-  workflow_dispatch:
+  workflow_dispatch:
+    inputs:
+      ref:
+        description: Git ref (branch name) to build (e.g., feature/my-branch)
+        required: true
+        type: string
+      pr:
+        description: Optional PR number to comment on (if running outside a PR event)
+        required: false
+        type: number

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
.github/workflows/preview.yml lines 3-6: the workflow currently enables
pull_request_target and workflow_dispatch but lacks dispatch inputs and allows
noisy PR triggers; add explicit workflow_dispatch inputs (e.g., ref or pr_number
and optional workflow_run flags) so manual runs know which ref/PR to build, and
constrain pull_request_target with a types list (for example opened, reopened,
synchronize, ready_for_review) or additional filters (branches or paths) to
reduce noisy triggers; update any jobs/steps that read the ref/pr to consume the
new inputs when present.

Comment on lines +19 to +21
"expo-haptics": "^14.1.4",
"expo-image-picker": "~16.0.2",
"expo-linear-gradient": "^14.1.5",
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Confirm Expo SDK 53 compatibility and prefer expo-managed pinning via expo install

The new modules look appropriate for SDK 53. To prevent version drift and ensure native compatibility, prefer pinning via expo install which resolves versions aligned with expo@~53.0.20.

You can verify and align versions with:


🏁 Script executed:

#!/bin/bash
# Use expo-managed versions for SDK 53
npx expo install expo-haptics expo-linear-gradient

# Show resolved versions for confirmation
cat package.json | jq '.dependencies | { "expo": .expo, "expo-haptics": ."expo-haptics", "expo-linear-gradient": ."expo-linear-gradient" }'

Length of output: 1151


🏁 Script executed:

#!/bin/bash
set -eo pipefail

echo "Current working directory: $(pwd)"
echo
echo "List top-level files:"
ls -la || true
echo
echo "Searching for package.json files (maxdepth 6):"
find . -maxdepth 6 -type f -name package.json -print || true
echo

TARGET="frontend/package.json"
if [ -f "$TARGET" ]; then
  echo "----- Found $TARGET; printing lines 1-120 -----"
  sed -n '1,120p' "$TARGET" || true
  echo
  echo "----- Showing dependencies via jq -----"
  jq '.dependencies, .devDependencies' "$TARGET" || true
else
  echo "$TARGET not found"
fi

Length of output: 3913


Action: run expo install in frontend to pin SDK‑53 managed versions

I verified frontend/package.json uses expo "~53.0.20" and includes the new modules but they are caret/tilde pinned — prefer expo-managed pins to ensure native compatibility.

  • File: frontend/package.json — relevant deps:
    "expo": "~53.0.20",
    "expo-haptics": "^14.1.4",
    "expo-image-picker": "~16.0.2",
    "expo-linear-gradient": "^14.1.5",
    

Run (from repo root):
cd frontend && npx expo install expo-haptics expo-image-picker expo-linear-gradient

Then commit the updated package.json/package-lock.json so versions are locked to Expo SDK 53.

🤖 Prompt for AI Agents
In frontend/package.json around lines 19 to 21 the Expo packages are pinned with
caret/tilde ranges instead of using Expo-managed exact versions; run from repo
root: cd frontend && npx expo install expo-haptics expo-image-picker
expo-linear-gradient to let Expo pin the correct SDK‑53 compatible versions,
then commit the updated frontend/package.json and frontend/package-lock.json so
the native-compatible versions are locked.

Comment on lines +76 to +83
const renderExpense = ({ item, index }) => {
const userSplit = item.splits.find((s) => s.userId === user._id);
const userShare = userSplit ? userSplit.amount : 0;
const paidByMe = (item.paidBy || item.createdBy) === user._id;
const net = paidByMe ? item.amount - userShare : -userShare;

let balanceText;
let balanceColor = "black";
let statusType = 'settled';
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard against absent splits array

item.splits may be undefined depending on backend data. Safe-guard the find call.

-    const userSplit = item.splits.find((s) => s.userId === user._id);
+    const userSplit = item.splits?.find?.((s) => s.userId === user._id);
📝 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
const renderExpense = ({ item, index }) => {
const userSplit = item.splits.find((s) => s.userId === user._id);
const userShare = userSplit ? userSplit.amount : 0;
const paidByMe = (item.paidBy || item.createdBy) === user._id;
const net = paidByMe ? item.amount - userShare : -userShare;
let balanceText;
let balanceColor = "black";
let statusType = 'settled';
const renderExpense = ({ item, index }) => {
const userSplit = item.splits?.find?.((s) => s.userId === user._id);
const userShare = userSplit ? userSplit.amount : 0;
const paidByMe = (item.paidBy || item.createdBy) === user._id;
const net = paidByMe ? item.amount - userShare : -userShare;
let balanceText;
let statusType = 'settled';
🤖 Prompt for AI Agents
In frontend/screens/GroupDetailsScreen.js around lines 76 to 83, the code calls
item.splits.find(...) but item.splits can be undefined; change the lookup to
safely handle a missing splits array (e.g., use optional chaining or default to
an empty array: item.splits?.find(...) or (item.splits || []).find(...)) and
ensure userShare remains 0 when no split is found so subsequent calculations
(paidByMe, net, statusType) don't throw.

Comment on lines +359 to 360
gap: spacing.md,
},
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid using CSS gap in RN styles (compat issues)

gap is not reliably supported across RN versions/platforms. Use explicit margins.

-  settlementContainer: {
-    gap: spacing.md,
-  },
+  settlementContainer: {},
   receiveSection: {
     borderRadius: borderRadius.md,
     padding: spacing.md,
+    marginTop: spacing.md,
   },

Also applies to: 382-384

🤖 Prompt for AI Agents
In frontend/screens/GroupDetailsScreen.js around lines 359-360 (and also apply
same change for lines 382-384), the style uses CSS property "gap" which is
unreliable in React Native; replace gap with explicit spacing by removing gap
and applying margins to child elements (e.g., set marginRight/marginBottom on
children or add container-specific styles like rowItem and columnItem with
appropriate margin) and update the components that consume this style so spacing
is achieved via explicit margin values from spacing.md instead of the gap
property.

Comment on lines +376 to +387
{ICON_CHOICES.map((i) => (
<TouchableOpacity
key={i}
style={[
styles.iconButton,
icon === i && styles.iconButtonSelected
]}
onPress={() => setIcon(i)}
disabled={!isAdmin}
>
<Text style={styles.iconText}>{i}</Text>
</TouchableOpacity>
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid conflicting selections: clear previously picked image when choosing an emoji

If a user had picked an image and then taps an emoji, onSave still prioritizes the image. Clear pickedImage on emoji selection so the UI state matches user intent.

-                    <TouchableOpacity
+                    <TouchableOpacity
                       key={i}
                       style={[
                         styles.iconButton,
                         icon === i && styles.iconButtonSelected
                       ]}
-                      onPress={() => setIcon(i)}
+                      onPress={() => {
+                        setIcon(i);
+                        setPickedImage(null);
+                      }}
                       disabled={!isAdmin}
                     >
📝 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
{ICON_CHOICES.map((i) => (
<TouchableOpacity
key={i}
style={[
styles.iconButton,
icon === i && styles.iconButtonSelected
]}
onPress={() => setIcon(i)}
disabled={!isAdmin}
>
<Text style={styles.iconText}>{i}</Text>
</TouchableOpacity>
{ICON_CHOICES.map((i) => (
<TouchableOpacity
key={i}
style={[
styles.iconButton,
icon === i && styles.iconButtonSelected
]}
onPress={() => {
setIcon(i);
setPickedImage(null);
}}
disabled={!isAdmin}
>
<Text style={styles.iconText}>{i}</Text>
</TouchableOpacity>
🤖 Prompt for AI Agents
In frontend/screens/GroupSettingsScreen.js around lines 376 to 387, selecting an
emoji currently only calls setIcon(i) so a previously picked image remains and
onSave still prefers the image; update the emoji onPress handler to also clear
the pickedImage state (call setPickedImage(null)) when an emoji is chosen so the
UI state and save behavior reflect the user’s selection.

Comment on lines +5 to +39
// Light tap for button presses
light: () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
},

// Medium impact for card selections
medium: () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
},

// Heavy impact for important actions
heavy: () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
},

// Success feedback
success: () => {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
},

// Warning feedback
warning: () => {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
},

// Error feedback
error: () => {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
},

// Selection feedback
selection: () => {
Haptics.selectionAsync();
},
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Return the promises from Haptics APIs to enable awaiting and error handling

Currently the calls fire-and-forget, which makes it harder for callers to await or handle failures (simulators/web). Returning the promises improves flexibility and avoids unhandled rejections if upstream chooses to await.

Apply this diff to return the underlying promises:

   // Light tap for button presses
   light: () => {
-    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+    return Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
   },

   // Medium impact for card selections
   medium: () => {
-    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
+    return Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
   },

   // Heavy impact for important actions
   heavy: () => {
-    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
+    return Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
   },

   // Success feedback
   success: () => {
-    Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
+    return Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
   },

   // Warning feedback
   warning: () => {
-    Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
+    return Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
   },

   // Error feedback
   error: () => {
-    Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
+    return Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
   },

   // Selection feedback
   selection: () => {
-    Haptics.selectionAsync();
+    return Haptics.selectionAsync();
   },
📝 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
// Light tap for button presses
light: () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
},
// Medium impact for card selections
medium: () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
},
// Heavy impact for important actions
heavy: () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
},
// Success feedback
success: () => {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
},
// Warning feedback
warning: () => {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
},
// Error feedback
error: () => {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
},
// Selection feedback
selection: () => {
Haptics.selectionAsync();
},
};
// Light tap for button presses
light: () => {
return Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
},
// Medium impact for card selections
medium: () => {
return Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
},
// Heavy impact for important actions
heavy: () => {
return Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
},
// Success feedback
success: () => {
return Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
},
// Warning feedback
warning: () => {
return Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
},
// Error feedback
error: () => {
return Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
},
// Selection feedback
selection: () => {
return Haptics.selectionAsync();
},
};
🤖 Prompt for AI Agents
In frontend/utils/haptics.js around lines 5 to 39, each helper currently calls
Haptics APIs without returning their promises; update each function to return
the promise from Haptics (e.g., return Haptics.impactAsync(...) or return
Haptics.notificationAsync(...) and return Haptics.selectionAsync()) so callers
can await and handle errors, ensuring consistent promise propagation for all
light, medium, heavy, success, warning, error, and selection functions.

Comment on lines +47 to +63
return (
<IconButton
icon={icon}
size={size}
iconColor={disabled ? colors.onSurfaceMuted : iconStyle.iconColor}
onPress={onPress}
disabled={disabled}
style={[
styles.iconButton,
{
backgroundColor: disabled ? colors.outlineVariant : iconStyle.backgroundColor,
},
style,
]}
{...props}
/>
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use containerColor for IconButton background instead of style backgroundColor

react-native-paper exposes containerColor to control the background; using style backgroundColor can be inconsistent and bypass states. Also keeps semantics with disabled state.

   return (
     <IconButton
       icon={icon}
       size={size}
       iconColor={disabled ? colors.onSurfaceMuted : iconStyle.iconColor}
       onPress={onPress}
       disabled={disabled}
-      style={[
-        styles.iconButton,
-        {
-          backgroundColor: disabled ? colors.outlineVariant : iconStyle.backgroundColor,
-        },
-        style,
-      ]}
+      containerColor={disabled ? colors.outlineVariant : iconStyle.backgroundColor}
+      style={[styles.iconButton, style]}
       {...props}
     />
   );
📝 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
return (
<IconButton
icon={icon}
size={size}
iconColor={disabled ? colors.onSurfaceMuted : iconStyle.iconColor}
onPress={onPress}
disabled={disabled}
style={[
styles.iconButton,
{
backgroundColor: disabled ? colors.outlineVariant : iconStyle.backgroundColor,
},
style,
]}
{...props}
/>
);
return (
<IconButton
icon={icon}
size={size}
iconColor={disabled ? colors.onSurfaceMuted : iconStyle.iconColor}
onPress={onPress}
disabled={disabled}
containerColor={disabled ? colors.outlineVariant : iconStyle.backgroundColor}
style={[styles.iconButton, style]}
{...props}
/>
);
🤖 Prompt for AI Agents
In frontend/utils/icons.js around lines 47 to 63, the IconButton currently sets
backgroundColor via the style prop which can bypass react-native-paper state
handling; replace that inline style backgroundColor with the IconButton prop
containerColor. Remove backgroundColor from the style object (leave other style
entries intact), and pass containerColor={disabled ? colors.outlineVariant :
iconStyle.backgroundColor} while keeping the existing disabled, iconColor, size,
onPress, spread props and the external style array.

Comment on lines +110 to +138
export const StatusIcon = ({ status, size = 20 }) => {
const getStatusConfig = () => {
switch (status) {
case 'settled':
return { icon: 'check-circle', color: colors.success };
case 'owed':
return { icon: 'arrow-up-circle', color: colors.warning };
case 'owes':
return { icon: 'arrow-down-circle', color: colors.error };
case 'pending':
return { icon: 'clock-outline', color: colors.onSurfaceVariant };
case 'admin':
return { icon: 'crown', color: colors.primary };
default:
return { icon: 'help-circle', color: colors.onSurfaceVariant };
}
};

const config = getStatusConfig();

return (
<IconButton
icon={config.icon}
size={size}
iconColor={config.color}
style={styles.statusIcon}
/>
);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid using IconButton for non-interactive status indicator; at least disable it

StatusIcon is decorative/non-interactive. Using IconButton suggests interactivity. If you want to keep Paper primitives, mark it disabled to avoid press/ripples and accessibility confusion.

   return (
     <IconButton
       icon={config.icon}
       size={size}
       iconColor={config.color}
       style={styles.statusIcon}
+      disabled
+      accessibilityRole="image"
+      accessibilityLabel={`${status} status`}
     />
   );

Alternatively, render MaterialCommunityIcons directly for a lightweight static icon.

📝 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 StatusIcon = ({ status, size = 20 }) => {
const getStatusConfig = () => {
switch (status) {
case 'settled':
return { icon: 'check-circle', color: colors.success };
case 'owed':
return { icon: 'arrow-up-circle', color: colors.warning };
case 'owes':
return { icon: 'arrow-down-circle', color: colors.error };
case 'pending':
return { icon: 'clock-outline', color: colors.onSurfaceVariant };
case 'admin':
return { icon: 'crown', color: colors.primary };
default:
return { icon: 'help-circle', color: colors.onSurfaceVariant };
}
};
const config = getStatusConfig();
return (
<IconButton
icon={config.icon}
size={size}
iconColor={config.color}
style={styles.statusIcon}
/>
);
};
export const StatusIcon = ({ status, size = 20 }) => {
const getStatusConfig = () => {
switch (status) {
case 'settled':
return { icon: 'check-circle', color: colors.success };
case 'owed':
return { icon: 'arrow-up-circle', color: colors.warning };
case 'owes':
return { icon: 'arrow-down-circle', color: colors.error };
case 'pending':
return { icon: 'clock-outline', color: colors.onSurfaceVariant };
case 'admin':
return { icon: 'crown', color: colors.primary };
default:
return { icon: 'help-circle', color: colors.onSurfaceVariant };
}
};
const config = getStatusConfig();
return (
<IconButton
icon={config.icon}
size={size}
iconColor={config.color}
style={styles.statusIcon}
disabled
accessibilityRole="image"
accessibilityLabel={`${status} status`}
/>
);
};
🤖 Prompt for AI Agents
In frontend/utils/icons.js around lines 110 to 138, the StatusIcon currently
renders an interactive IconButton for a decorative, non-interactive status
indicator; update the implementation to either (A) keep IconButton but mark it
non-interactive by adding the disabled prop, remove onPress (if any), disable
ripple/feedback and set accessibilityRole to "image" and accessible={true} with
an appropriate accessibilityLabel, or (B) replace IconButton with a lightweight
static icon component (e.g., MaterialCommunityIcons) that uses the same icon
name, size and color to avoid suggesting interactivity; choose one approach and
apply consistent props so the icon is truly non-interactive and accessible.

Comment on lines +145 to +151
keyboardType="numeric"
leftIcon={
<Text style={styles.currencySymbol}>{currency}</Text>
}
style={style}
{...props}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use decimal keyboard for currency entry

keyboardType="numeric" often omits the decimal separator on iOS. Use decimal-pad for amounts, and consider locale-aware separators if needed.

-      keyboardType="numeric"
+      keyboardType="decimal-pad"
📝 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
keyboardType="numeric"
leftIcon={
<Text style={styles.currencySymbol}>{currency}</Text>
}
style={style}
{...props}
/>
keyboardType="decimal-pad"
leftIcon={
<Text style={styles.currencySymbol}>{currency}</Text>
}
style={style}
{...props}
/>
🤖 Prompt for AI Agents
In frontend/utils/inputs.js around lines 145 to 151, the input uses
keyboardType="numeric" which can omit the decimal separator on iOS; change the
keyboardType to "decimal-pad" for currency/amount inputs, and ensure any
parsing/validation accepts locale decimal separators (e.g., '.' and ',') or
normalize input before numeric conversion. If you need platform-specific
fallbacks, use Platform.OS checks to set "decimal-pad" on iOS/Android and a safe
fallback on older devices.

Comment on lines 157 to 172
export const animations = {
// Timing configurations
timing: {
short: 200,
medium: 300,
long: 500,
},

// Easing functions
easing: {
easeOut: 'ease-out',
easeIn: 'ease-in',
easeInOut: 'ease-in-out',
spring: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
},
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Easing tokens are strings; provide concrete Easing functions to avoid misuse

Animated.timing expects Easing functions, not string names. If consumers pass these strings directly to Animated, it will be incorrect. Export Easing functions as tokens.

Apply this diff, which provides RN Easing functions while keeping timing tokens:

+import { Easing } from 'react-native';
...
 export const animations = {
   // Timing configurations
   timing: {
     short: 200,
     medium: 300,
     long: 500,
   },
 
   // Easing functions
   easing: {
-    easeOut: 'ease-out',
-    easeIn: 'ease-in',
-    easeInOut: 'ease-in-out',
-    spring: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
+    easeOut: Easing.out(Easing.cubic),
+    easeIn: Easing.in(Easing.cubic),
+    easeInOut: Easing.inOut(Easing.cubic),
+    spring: Easing.bezier(0.68, -0.55, 0.265, 1.55),
   },
 };

If you’re standardizing on Reanimated, import Easing from react-native-reanimated instead.

📝 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 animations = {
// Timing configurations
timing: {
short: 200,
medium: 300,
long: 500,
},
// Easing functions
easing: {
easeOut: 'ease-out',
easeIn: 'ease-in',
easeInOut: 'ease-in-out',
spring: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
},
};
import { Easing } from 'react-native';
export const animations = {
// Timing configurations
timing: {
short: 200,
medium: 300,
long: 500,
},
// Easing functions
easing: {
easeOut: Easing.out(Easing.cubic),
easeIn: Easing.in(Easing.cubic),
easeInOut: Easing.inOut(Easing.cubic),
spring: Easing.bezier(0.68, -0.55, 0.265, 1.55),
},
};
🤖 Prompt for AI Agents
In frontend/utils/theme.js around lines 157 to 172, the easing tokens are
currently string names but Animated.timing expects Easing functions; replace the
string values with actual Easing functions (import Easing from react-native or
from react-native-reanimated if you standardize on Reanimated), e.g. map
easeOut/easeIn/easeInOut to
Easing.out(Easing.ease)/Easing.in(Easing.ease)/Easing.inOut(Easing.ease) and
replace the spring string with a cubic-bezier equivalent via Easing.bezier(0.68,
-0.55, 0.265, 1.55); keep the timing tokens unchanged and export the animations
object so consumers can pass animations.easing.* directly to Animated APIs.

…smorphism and enhanced UX

- Introduced advanced card components: GlassCard, ExpenseCard, GroupSummaryCard, and QuickActionCard.
- Enhanced GroupCard for backward compatibility with new design.
- Updated theme colors and typography to align with "Expressive Minimalism" and Gen Z preferences.
- Implemented glassmorphism effects and improved haptic feedback interactions.
- Refactored styles and animations for a cohesive user experience.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

♻️ Duplicate comments (5)
frontend/screens/HomeScreen.js (3)

546-551: Replace gap in quickActionsGrid with margins (RN support is spotty)

Use per-item margins to simulate spacing instead of gap.

Apply this diff as a simple, robust alternative:

   quickActionsGrid: {
     flexDirection: 'row',
     flexWrap: 'wrap',
-    gap: spacing.md,
   },
   quickActionCard: {
-    width: (width - spacing.lg * 3) / 2,
+    width: (width - spacing.lg * 3) / 2,
+    marginRight: spacing.md,
+    marginBottom: spacing.md,
   },

Optionally remove the right margin for every second item in the render function based on index.


583-584: Remove gap from groupsList; rely on item margins

You already set vertical spacing via groupItemContainer. Drop gap to avoid platform issues.

   groupsList: {
-    gap: spacing.md,
   },

614-616: Modal actions: avoid gap and space buttons with margins

RN gap isn’t reliably supported. Remove it and add margin to the first button.

   modalActions: {
     flexDirection: 'row',
-    gap: spacing.md,
   },

Update the first button style inline:

-              <Button
+              <Button
                 title="Cancel"
                 onPress={hideModal}
                 variant="ghost"
-                style={styles.modalActionButton}
+                style={[styles.modalActionButton, { marginRight: spacing.md }]}
               />

Also applies to: 427-442

frontend/utils/theme.js (1)

224-231: Replace string easing tokens with concrete Easing functions

Animated/Reanimated expect Easing functions, not string names. Exporting strings invites misuse and runtime errors when passed directly to timing APIs.

Apply this diff:

+import { Easing } from 'react-native';
@@
 export const animations = {
   timing: {
     fast: 150,     // Button interactions, quick feedback
     normal: 250,   // Screen transitions, modal appearances
     slow: 300,     // Complex transitions
     loading: 1500, // Success celebrations
   },
 
   easing: {
-    easeOut: 'ease-out',
-    easeIn: 'ease-in',
-    easeInOut: 'ease-in-out',
-    spring: 'cubic-bezier(0.4, 0, 0.2, 1)', // Material Design standard
-    bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
+    easeOut: Easing.out(Easing.cubic),
+    easeIn: Easing.in(Easing.cubic),
+    easeInOut: Easing.inOut(Easing.cubic),
+    spring: Easing.bezier(0.4, 0, 0.2, 1), // Material Design standard
+    bounce: Easing.bezier(0.68, -0.55, 0.265, 1.55),
   },
 };

Also applies to: 1-3

frontend/utils/cards.js (1)

4-21: Use the shared currency formatter for consistency

Raw numeric amounts degrade readability and locale consistency. Import and use the existing currency formatter.

Apply this diff:

@@
 import * as Haptics from 'expo-haptics';
 import { LinearGradient } from 'expo-linear-gradient';
 import { useRef } from 'react';
@@
-    View
+    View
 } from 'react-native';
 import {
     borderRadius,
     colors,
     shadows,
     spacing,
     typography
 } from './theme';
+import { formatCurrency } from './currency';
@@
-      <Text style={{
+      <Text style={{
         ...typography.display,
         fontSize: 28,
         color: colors.brand.accent,
         marginBottom: spacing.sm,
       }}>
-        ${amount}
+        {formatCurrency(amount)}
       </Text>
@@
-            <Text style={{
+            <Text style={{
               ...typography.h2,
               color: '#FFFFFF',
             }}>
-              ${totalExpenses}
+              {formatCurrency(totalExpenses)}
             </Text>
@@
-          <Text style={{
+          <Text style={{
             ...typography.h2,
             color: '#FFFFFF',
           }}>
-            ${Math.abs(yourBalance).toFixed(2)}
+            {formatCurrency(Math.abs(yourBalance))}
           </Text>
@@
-    if (settlementStatus.netBalance > 0) {
+    if (settlementStatus.netBalance > 0) {
       return {
-        text: `💰 You're owed ${settlementStatus.netBalance}`,
+        text: `💰 You're owed ${formatCurrency(settlementStatus.netBalance)}`,
         type: 'success',
         icon: '💰'
       };
     } else if (settlementStatus.netBalance < 0) {
       return {
-        text: `💳 You owe ${Math.abs(settlementStatus.netBalance)}`,
+        text: `💳 You owe ${formatCurrency(Math.abs(settlementStatus.netBalance))}`,
         type: 'warning',
         icon: '💳'
       };
     }

Also applies to: 188-196, 297-302, 357-362, 479-491

🧹 Nitpick comments (26)
frontend/A Design Blueprint for a Modern, Minimalist Expens.md (8)

7-7: Tighten punctuation around “self-expression”

Remove the stray space before the comma.

Apply this diff:

-... personalization, and vibrant self-expression , and the functional clarity ...
+... personalization, and vibrant self-expression, and the functional clarity ...

20-20: Concise phrasing: replace “out of touch with”

“Out of touch with” reads wordy; “misaligned with” is tighter and direct.

-... fundamentally out of touch with the expectations of its intended users ...
+... fundamentally misaligned with the expectations of its intended users ...

24-24: Strengthen wording (“deep analysis”)

“Deep” is vague. Consider “thorough” or “comprehensive” for precision.

-... begins with a deep analysis of the target user ...
+... begins with a comprehensive analysis of the target user ...

40-40: Sharper verb than “make … faster”

Prefer “accelerate”/“speed” to reduce verbosity.

-... allowing users to process information and make decisions faster ...
+... allowing users to process information and accelerate decision-making ...

180-180: Minor grammar polish

Insert “and” or split the sentence for clarity in the list of button states.

-... default, pressed (...), disabled (...), loading (...)
+... default; pressed (...); disabled (...); and loading (...)

210-210: Minor grammar polish

Add a comma after “right” for readability.

-Action icons (e.g., for search or settings) will be placed on the right and kept to a minimum...
+Action icons (e.g., for search or settings) will be placed on the right, and kept to a minimum...

251-251: Minor grammar polish

Use a period to separate the list item sentences for readability.

-... less disruptive than navigating to a full new page. - **Input-First Design:** ...
+... less disruptive than navigating to a full new page.
+**Input-First Design:** ...

285-285: Micro-typo: plural agreement (“feel” vs. “feels”)

Ensure the subject agrees with the verb.

-... durations will be kept short (...) to ensure the interface feels snappy ...
+... durations will be kept short (...) to ensure the interface feels snappy ...

Note: If “durations” is the subject, use “feel”; if “the interface” is the subject, “feels” is correct. Current sentence structure looks fine; re-check for consistency in the final draft.

frontend/components/core/Button.js (2)

7-7: Use theme typography instead of hardcoding 'Inter'

Hardcoding the font family diverges from the design tokens and can break if the font isn’t loaded. Use the theme’s typography.

-import { borderRadius, colors, shadows, spacing } from '../../utils/theme';
+import { borderRadius, colors, shadows, spacing, typography } from '../../utils/theme';
-  const textStyleConfig = {
+  const textStyleConfig = {
     fontSize: currentSize.fontSize,
     fontWeight: currentSize.fontWeight,
     color: currentVariant.textColor,
-    fontFamily: 'Inter',
+    fontFamily: typography.body.fontFamily,
     ...textStyle,
   };

Also applies to: 110-116


151-163: Shadow layering on gradient container

You’re applying shadow on the outer Touchable and later zeroing shadow on the gradient. This can create subtle visual mismatch between states. Consider moving shadows exclusively to the outer container and leaving the gradient container shadowless, ensuring consistency.

No code change required; just verify the desired look across platforms (iOS elevation vs. Android).

frontend/screens/ModernAddExpenseScreen.js (3)

391-395: Prefer pixel width for Animated progress bar

Animating to percentage strings is flaky in RN Animated. Use numeric width based on screen width.

-            width: progressAnim.interpolate({
-              inputRange: [0, 1],
-              outputRange: ['0%', '100%'],
-            }),
+            width: progressAnim.interpolate({
+              inputRange: [0, 1],
+              outputRange: [0, screenWidth],
+            }),

124-135: Add validation for positive amount

Prevent submission if amount is not a positive number. This avoids creating zero/negative-amount expenses.

-    if (!description || !amount) {
+    if (!description || !amount || isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) {
       await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
-      Alert.alert("Missing Information", "Please fill in the expense description and amount.");
+      Alert.alert("Invalid Amount", "Please enter a valid positive amount and description.");
       return;
     }

441-444: Avoid using gap in RN styles (inconsistent support)

React Native’s gap has limited/partial support across versions and platforms. Use margins on children to achieve spacing.

Example adjustments:

  • Remove gap from grids/lists.
  • Add marginRight/marginBottom to child items (e.g., payerOption, splitMethodOption, membersList items), ensuring last-in-row items don’t add extra right margin.

Also applies to: 482-486, 514-516

frontend/components/core/Input.js (2)

233-249: CurrencyInput: edge cases for input normalization

Entering “.” or “.5” yields a leading dot. Consider normalizing to “0.” and “0.5”. Also consider trimming leading zeros (“000.50” → “0.50”).

Example normalization inside formatCurrency:

-    return cleaned;
+    // Normalize leading dot -> "0."
+    if (cleaned.startsWith('.')) return '0' + cleaned;
+    // Normalize multiple leading zeros
+    const m = cleaned.match(/^0+([0-9])/);
+    if (m) return '0' + cleaned.slice(cleaned.indexOf(m[1]));
+    return cleaned;

Also applies to: 251-255


39-49: Haptic on focus: consider rate-limiting

Triggering haptics on every focus can feel noisy. Consider gating to specific variants or adding a cooldown to avoid rapid consecutive triggers on quick focus switches.

No code change required—just a product/UX consideration.

frontend/screens/HomeScreen.js (2)

111-130: N+1 network calls for settlements per group

Fetching optimized settlements for every group on the main thread can be slow on large lists. Consider:

  • Deferring per-group settlements until the card is near viewport (onViewableItemsChanged).
  • Caching results and refreshing on pull-to-refresh.
  • Batching where possible.

I can draft a lazy-loading approach that computes settlements on demand—say the word.


257-267: memberCount fallback of 2 is arbitrary

If available, prefer deriving from data (e.g., group.members?.length) or omit the count rather than defaulting to 2.

No code change provided due to unknown shape; verify the best available source.

frontend/utils/theme.js (3)

255-269: Fix alpha hex vs. comment mismatch (20% opacity != '20' hex suffix)

The comments say “20% opacity” but appending '20' to hex is ~12.5% alpha. If you want 20%, use '33' (0x33).

Apply this diff to align values with comments:

-    primaryContainer: `${colors.brand.accent}20`, // 20% opacity
+    primaryContainer: `${colors.brand.accent}33`, // 20% opacity
@@
-    secondaryContainer: `${colors.brand.accentAlt}20`,
+    secondaryContainer: `${colors.brand.accentAlt}33`,

Optional: avoid hand-appending alpha with a helper to prevent future mistakes:

// helper
export const withAlpha = (hex, alpha /* 0..1 */) => {
  const a = Math.round(alpha * 255);
  const aa = a.toString(16).padStart(2, '0');
  return `${hex}${aa}`;
};

// usage
primaryContainer: withAlpha(colors.brand.accent, 0.2),

251-271: Paper theme is defined but not applied in the provider

App.js currently renders without a theme. Wire in paperTheme so the new tokens actually take effect.

Proposed change in frontend/App.js (illustrative, outside this file):

import { Provider as PaperProvider } from 'react-native-paper';
import { paperTheme } from './utils/theme';

function App() {
  return (
    <AuthProvider>
-     <PaperProvider>
+     <PaperProvider theme={paperTheme}>
        <AppNavigator />
      </PaperProvider>
    </AuthProvider>
  );
}

50-86: CSS custom properties won’t apply in React Native

These tokens are fine as documentation/centralized constants, but RN won’t interpret CSS variables or 'px' units. If intended only for web/native-web, consider fencing or documenting scope to avoid confusion for RN consumers.

frontend/utils/cards.js (2)

7-13: Remove unused Dimensions import and screenWidth

They’re not used in this module.

@@
 import {
-    Animated,
-    Dimensions,
+    Animated,
     Text,
     TouchableOpacity,
     View
 } from 'react-native';
@@
-const { width: screenWidth } = Dimensions.get('window');

Also applies to: 22-23


24-32: glassEffect prop is unused

Either remove it from the API or wire it to a blur/background treatment to avoid dead props.

frontend/components/navigation/ModernNavigation.js (4)

29-37: Include safe-area bottom inset in the tab bar padding

Without accounting for the bottom inset, the tab bar may feel cramped on devices with a home indicator.

Apply this diff (and import useSafeAreaInsets):

@@
-import {
+import {
     Dimensions,
     Text,
     TouchableOpacity,
     View
 } from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@
-const ModernTabBar = ({ 
+const ModernTabBar = ({ 
   state, 
   descriptors, 
   navigation,
   style,
 }) => {
+  const insets = useSafeAreaInsets();
   return (
     <View style={[
       {
         backgroundColor: colors.glass.background,
         borderTopWidth: 1,
         borderTopColor: colors.glass.border,
-        paddingBottom: spacing.sm,
+        paddingBottom: Math.max(spacing.sm, insets.bottom),
         paddingTop: spacing.sm,
         paddingHorizontal: spacing.md,
       },
       style
     ]}>

398-427: FAB position should respect safe-area insets

Ensure the FAB sits above the home indicator/notch on modern devices.

@@
-const FloatingActionButton = ({
+const FloatingActionButton = ({
   icon = '➕',
   onPress,
   position = 'bottom-right', // bottom-right, bottom-left, bottom-center
   style,
 }) => {
+  const insets = useSafeAreaInsets?.() ?? { bottom: 0 };
@@
-  const getPositionStyle = () => {
+  const getPositionStyle = () => {
     const baseStyle = {
       position: 'absolute',
-      bottom: spacing.xl,
+      bottom: spacing.xl + (insets.bottom || 0),
       zIndex: 1000,
     };

354-395: Label inside circular ActionButton will be clipped; render label below the button

The button has a fixed width/height and centers content; rendering the label within the same container will overflow or be clipped. Render the label as a sibling below the circular touch target.

Illustrative refactor:

-  return (
-    <TouchableOpacity
-      onPress={handlePress}
-      style={[
-        {
-          width: currentSize.width,
-          height: currentSize.height,
-          borderRadius: currentSize.width / 2,
-          alignItems: 'center',
-          justifyContent: 'center',
-          backgroundColor: currentVariant.backgroundColor,
-          borderWidth: variant !== 'ghost' ? 1 : 0,
-          borderColor: currentVariant.borderColor,
-          ...shadows.small,
-        },
-        style,
-      ]}
-    >
-      {typeof icon === 'string' ? (
-        <Text style={{
-          fontSize: currentSize.fontSize,
-          color: currentVariant.iconColor,
-        }}>
-          {icon}
-        </Text>
-      ) : (
-        icon
-      )}
-      
-      {label && (
-        <Text style={{
-          ...typography.caption,
-          color: currentVariant.iconColor,
-          marginTop: 2,
-          textAlign: 'center',
-        }}>
-          {label}
-        </Text>
-      )}
-    </TouchableOpacity>
-  );
+  return (
+    <View style={style}>
+      <TouchableOpacity
+        onPress={handlePress}
+        style={[
+          {
+            width: currentSize.width,
+            height: currentSize.height,
+            borderRadius: currentSize.width / 2,
+            alignItems: 'center',
+            justifyContent: 'center',
+            backgroundColor: currentVariant.backgroundColor,
+            borderWidth: variant !== 'ghost' ? 1 : 0,
+            borderColor: currentVariant.borderColor,
+            ...shadows.small,
+          },
+        ]}
+      >
+        {typeof icon === 'string' ? (
+          <Text style={{
+            fontSize: currentSize.fontSize,
+            color: currentVariant.iconColor,
+          }}>
+            {icon}
+          </Text>
+        ) : (
+          icon
+        )}
+      </TouchableOpacity>
+      {label && (
+        <Text style={{
+          ...typography.caption,
+          color: currentVariant.iconColor,
+          marginTop: 6,
+          textAlign: 'center',
+        }}>
+          {label}
+        </Text>
+      )}
+    </View>
+  );

55-66: Optional: support tabLongPress to align with React Navigation conventions

Emitting 'tabLongPress' improves parity with the default tab bar behavior.

@@
-          const onPress = async () => {
+          const onPress = async () => {
             await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
             
             const event = navigation.emit({
               type: 'tabPress',
               target: route.key,
             });
@@
+          const onLongPress = () => {
+            navigation.emit({
+              type: 'tabLongPress',
+              target: route.key,
+            });
+          };
@@
-            <TouchableOpacity
+            <TouchableOpacity
               key={route.key}
               onPress={onPress}
+              onLongPress={onLongPress}

Also applies to: 129-137

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between afde178 and 72fea4d.

📒 Files selected for processing (8)
  • frontend/A Design Blueprint for a Modern, Minimalist Expens.md (1 hunks)
  • frontend/components/core/Button.js (1 hunks)
  • frontend/components/core/Input.js (1 hunks)
  • frontend/components/navigation/ModernNavigation.js (1 hunks)
  • frontend/screens/HomeScreen.js (1 hunks)
  • frontend/screens/ModernAddExpenseScreen.js (1 hunks)
  • frontend/utils/cards.js (1 hunks)
  • frontend/utils/theme.js (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (6)
frontend/components/navigation/ModernNavigation.js (5)
frontend/screens/ModernAddExpenseScreen.js (1)
  • screenWidth (26-26)
frontend/screens/HomeScreen.js (1)
  • Dimensions (34-34)
frontend/utils/theme.js (2)
  • colors (4-48)
  • colors (4-48)
frontend/components/core/Button.js (1)
  • handlePress (22-31)
frontend/navigation/MainNavigator.js (4)
  • Tab (10-42)
  • MaterialCommunityIcons (35-37)
  • MaterialCommunityIcons (17-19)
  • MaterialCommunityIcons (26-28)
frontend/A Design Blueprint for a Modern, Minimalist Expens.md (2)
frontend/screens/AddExpenseScreen.js (2)
  • description (97-237)
  • handleSplitChange (265-329)
frontend/screens/GroupDetailsScreen.js (1)
  • userSplit (73-104)
frontend/screens/HomeScreen.js (8)
frontend/screens/AccountScreen.js (1)
  • styles (67-85)
frontend/context/AuthContext.js (5)
  • AuthContext (10-10)
  • AuthContext (10-10)
  • isLoading (16-16)
  • token (14-14)
  • user (13-13)
frontend/api/groups.js (6)
  • getOptimizedSettlements (5-6)
  • getOptimizedSettlements (5-6)
  • getGroups (3-3)
  • getGroups (3-3)
  • createGroup (21-21)
  • createGroup (21-21)
frontend/utils/gradients.js (1)
  • styles (104-117)
frontend/utils/cards.js (2)
  • QuickActionCard (370-447)
  • GroupSummaryCard (248-367)
frontend/utils/theme.js (8)
  • colors (4-48)
  • colors (4-48)
  • spacing (88-95)
  • spacing (88-95)
  • borderRadius (97-102)
  • borderRadius (97-102)
  • typography (105-169)
  • typography (105-169)
frontend/components/core/Button.js (1)
  • Button (9-179)
frontend/components/core/Input.js (1)
  • EnhancedTextInput (15-221)
frontend/components/core/Button.js (2)
frontend/utils/cards.js (2)
  • handlePress (53-58)
  • handlePress (457-460)
frontend/utils/theme.js (8)
  • spacing (88-95)
  • spacing (88-95)
  • colors (4-48)
  • colors (4-48)
  • shadows (172-213)
  • shadows (172-213)
  • borderRadius (97-102)
  • borderRadius (97-102)
frontend/utils/cards.js (2)
frontend/components/core/Button.js (2)
  • handlePress (22-31)
  • variantConfig (59-88)
frontend/utils/theme.js (10)
  • colors (4-48)
  • colors (4-48)
  • shadows (172-213)
  • shadows (172-213)
  • borderRadius (97-102)
  • borderRadius (97-102)
  • spacing (88-95)
  • spacing (88-95)
  • typography (105-169)
  • typography (105-169)
frontend/utils/theme.js (1)
frontend/App.js (1)
  • App (6-14)
🪛 LanguageTool
frontend/A Design Blueprint for a Modern, Minimalist Expens.md

[style] ~20-~20: ‘out of touch with’ might be wordy. Consider a shorter alternative.
Context: ...rsome, untrustworthy, and fundamentally out of touch with the expectations of its intended users,...

(EN_WORDINESS_PREMIUM_OUT_OF_TOUCH_WITH)


[style] ~24-~24: Consider a different adjective to strengthen your wording.
Context: ...plementation. The journey begins with a deep analysis of the target user, translatin...

(DEEP_PROFOUND)


[style] ~40-~40: Consider using a different verb to strengthen your wording.
Context: ...lowing users to process information and make decisions faster, combating the decision fatigue that cl...

(MAKE_XXX_FASTER)


[grammar] ~180-~180: There might be a mistake here.
Context: ...ckground (--color-background-secondary). - Tertiary/Text Button: For low-priority...

(QB_NEW_EN)


[grammar] ~210-~210: There might be a mistake here.
Context: ... outlined icons and regular weight text. - Headers: Screen headers will be clean ...

(QB_NEW_EN)


[grammar] ~236-~236: There might be a mistake here.
Context: ...group expenses," and "Settle up simply." - Security First: The sign-up/login scre...

(QB_NEW_EN)


[grammar] ~251-~251: There might be a mistake here.
Context: ...tive than navigating to a full new page. - Input-First Design: The screen's prima...

(QB_NEW_EN)


[grammar] ~285-~285: Ensure spelling is correct
Context: ...s will be kept short (typically between 150ms and 300ms) to ensure the interface feel...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~285-~285: Ensure spelling is correct
Context: ...kept short (typically between 150ms and 300ms) to ensure the interface feels snappy a...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🔇 Additional comments (1)
frontend/components/core/Input.js (1)

13-13: animations.timing.fast verified in theme — no action required

Confirmed that frontend/utils/theme.js exports animations and includes timing.fast (150).

  • Location: frontend/utils/theme.js — export const animations (lines ~216–218)

Comment on lines +93 to +107
// Base button style
const buttonStyle = {
borderRadius: borderRadius.md,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
minHeight: currentSize.minHeight,
paddingVertical: currentSize.paddingVertical,
paddingHorizontal: currentSize.paddingHorizontal,
width: fullWidth ? '100%' : 'auto',
opacity: disabled ? 0.6 : 1,
...currentVariant.shadowStyle,
...currentVariant,
...style,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid passing non-style keys into style objects

You’re spreading the entire currentVariant object into buttonStyle. Keys like useGradient, gradientColors, and textColor are not style props and pollute the style object.

Refactor to only apply style-related keys from the variant:

-  const buttonStyle = {
+  const { shadowStyle, backgroundColor, borderWidth, borderColor } = currentVariant;
+  const buttonStyle = {
     borderRadius: borderRadius.md,
     alignItems: 'center',
     justifyContent: 'center',
     flexDirection: 'row',
     minHeight: currentSize.minHeight,
     paddingVertical: currentSize.paddingVertical,
     paddingHorizontal: currentSize.paddingHorizontal,
     width: fullWidth ? '100%' : 'auto',
     opacity: disabled ? 0.6 : 1,
-    ...currentVariant.shadowStyle,
-    ...currentVariant,
+    ...(shadowStyle || {}),
+    ...(backgroundColor ? { backgroundColor } : {}),
+    ...(borderWidth ? { borderWidth } : {}),
+    ...(borderColor ? { borderColor } : {}),
     ...style,
   };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/components/core/Button.js around lines 93 to 107, the code currently
spreads the entire currentVariant into buttonStyle which mixes non-style keys
(useGradient, gradientColors, textColor) into the style object; change this to
only spread actual style properties by extracting non-style keys first (e.g.
const { useGradient, gradientColors, textColor, ...variantStyle } =
currentVariant) and then spread variantStyle (and currentVariant.shadowStyle if
needed) into buttonStyle, or alternatively explicitly pick known style keys from
currentVariant before spreading so only valid style props are included.

Comment on lines +141 to +165
// Render with gradient if specified
if (currentVariant.useGradient && !disabled) {
return (
<TouchableOpacity
onPress={handlePress}
disabled={disabled || loading}
activeOpacity={0.8}
style={[buttonStyle, { backgroundColor: 'transparent' }]}
{...props}
>
<LinearGradient
colors={currentVariant.gradientColors}
style={{
...buttonStyle,
shadowColor: 'transparent', // Remove shadow from gradient container
elevation: 0,
}}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<ButtonContent />
</LinearGradient>
</TouchableOpacity>
);
}
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

Disabled primary button loses its background (gradient gated by disabled)

When variant is “primary” and disabled is true, the gradient path is skipped, and no solid background is defined for the primary variant. The disabled primary button can render with a transparent background, harming legibility and affordance.

Apply this minimal fix to always render the gradient, relying on the existing opacity reduction for disabled:

-  // Render with gradient if specified
-  if (currentVariant.useGradient && !disabled) {
+  // Render with gradient if specified (even when disabled, rely on reduced opacity)
+  if (currentVariant.useGradient) {
📝 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
// Render with gradient if specified
if (currentVariant.useGradient && !disabled) {
return (
<TouchableOpacity
onPress={handlePress}
disabled={disabled || loading}
activeOpacity={0.8}
style={[buttonStyle, { backgroundColor: 'transparent' }]}
{...props}
>
<LinearGradient
colors={currentVariant.gradientColors}
style={{
...buttonStyle,
shadowColor: 'transparent', // Remove shadow from gradient container
elevation: 0,
}}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<ButtonContent />
</LinearGradient>
</TouchableOpacity>
);
}
// Render with gradient if specified (even when disabled, rely on reduced opacity)
if (currentVariant.useGradient) {
return (
<TouchableOpacity
onPress={handlePress}
disabled={disabled || loading}
activeOpacity={0.8}
style={[buttonStyle, { backgroundColor: 'transparent' }]}
{...props}
>
<LinearGradient
colors={currentVariant.gradientColors}
style={{
...buttonStyle,
shadowColor: 'transparent', // Remove shadow from gradient container
elevation: 0,
}}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<ButtonContent />
</LinearGradient>
</TouchableOpacity>
);
}
🤖 Prompt for AI Agents
frontend/components/core/Button.js around lines 141-165: the current conditional
skips the gradient when disabled, causing a transparent background for disabled
primary buttons; remove the "!disabled" gate so the LinearGradient branch always
renders for variants with useGradient, keep TouchableOpacity's disabled and
loading props as-is so the built-in opacity/reduced-affordance behavior remains,
and leave the gradient colors/styles unchanged (you may keep the transparent
background on the TouchableOpacity wrapper and elevation/shadow adjustments on
the LinearGradient).

Comment on lines +7 to +14
import {
Alert,
Animated,
Dimensions,
ScrollView,
StyleSheet,
View
} from "react-native";
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

TouchableOpacity is used but not imported

Multiple usages of TouchableOpacity exist (e.g., payer and method selections), but it’s not imported from react-native. This will throw at runtime.

Apply this diff:

 import {
     Alert,
     Animated,
     Dimensions,
+    TouchableOpacity,
     ScrollView,
     StyleSheet,
     View
 } from "react-native";

Also applies to: 279-303

🤖 Prompt for AI Agents
In frontend/screens/ModernAddExpenseScreen.js around lines 7 to 14 (and
similarly lines 279 to 303), TouchableOpacity is used but not imported from
react-native; add TouchableOpacity to the named imports from react-native at the
top of the file so the component resolves at runtime, e.g., include
TouchableOpacity alongside Alert, Animated, Dimensions, ScrollView, StyleSheet,
View in the import list.

Comment on lines +145 to +156
try {
const expenseData = {
description,
amount: parseFloat(amount),
paidBy: payerId,
splitMethod,
...(splitMethod === "equal" && { selectedMembers }),
...(splitMethod === "percentage" && { percentages }),
...(splitMethod === "shares" && { shares }),
...(splitMethod === "exact" && { exactAmounts }),
};

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

Payload shape likely incompatible with existing createExpense API

The previous AddExpenseScreen constructs splitType and splits (equal/unequal/percentage). Here you’re sending { splitMethod, percentages|shares|exactAmounts|selectedMembers }, which likely won’t match the backend contract and will break expense creation.

Construct splitType and splits before calling createExpense. Example fix:

-      const expenseData = {
-        description,
-        amount: parseFloat(amount),
-        paidBy: payerId,
-        splitMethod,
-        ...(splitMethod === "equal" && { selectedMembers }),
-        ...(splitMethod === "percentage" && { percentages }),
-        ...(splitMethod === "shares" && { shares }),
-        ...(splitMethod === "exact" && { exactAmounts }),
-      };
+      const numericAmount = parseFloat(amount);
+      const allMembers = members.map(m => m.userId);
+      const participants = splitMethod === 'equal'
+        ? allMembers.filter(id => selectedMembers[id])
+        : allMembers;
+
+      let splits = [];
+      let splitType = splitMethod;
+
+      if (splitMethod === 'equal') {
+        const count = participants.length || 1;
+        const perHead = Math.floor((numericAmount / count) * 100) / 100;
+        let centsRemaining = Math.round(numericAmount * 100) - Math.round(perHead * 100) * count;
+        splits = participants.map((userId) => {
+          const addCent = centsRemaining-- > 0 ? 0.01 : 0;
+          return { userId, amount: perHead + addCent, type: 'equal' };
+        });
+        splitType = 'equal';
+      } else if (splitMethod === 'percentage') {
+        const totalPct = participants.reduce((sum, id) => sum + (parseFloat(percentages[id] || '0')), 0) || 100;
+        splits = participants.map((userId) => {
+          const pct = parseFloat(percentages[userId] || '0');
+          const amt = Math.round(((pct / totalPct) * numericAmount) * 100) / 100;
+          return { userId, amount: amt, type: 'percentage' };
+        });
+        splitType = 'percentage';
+      } else if (splitMethod === 'shares') {
+        const totalShares = participants.reduce((sum, id) => sum + (parseFloat(shares[id] || '0')), 0) || 1;
+        splits = participants.map((userId) => {
+          const s = parseFloat(shares[userId] || '0');
+          const amt = Math.round(((s / totalShares) * numericAmount) * 100) / 100;
+          return { userId, amount: amt, type: 'unequal' };
+        });
+        splitType = 'unequal';
+      } else if (splitMethod === 'exact') {
+        splits = participants.map((userId) => ({
+          userId,
+          amount: Math.round(parseFloat(exactAmounts[userId] || '0') * 100) / 100,
+          type: 'unequal',
+        }));
+        splitType = 'unequal';
+      }
+
+      const expenseData = {
+        description,
+        amount: numericAmount,
+        paidBy: payerId,
+        splitType,
+        splits,
+      };

Follow-up: Validate this contract matches your backend expectations and legacy AddExpenseScreen behavior.

📝 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
try {
const expenseData = {
description,
amount: parseFloat(amount),
paidBy: payerId,
splitMethod,
...(splitMethod === "equal" && { selectedMembers }),
...(splitMethod === "percentage" && { percentages }),
...(splitMethod === "shares" && { shares }),
...(splitMethod === "exact" && { exactAmounts }),
};
try {
const numericAmount = parseFloat(amount);
const allMembers = members.map(m => m.userId);
const participants = splitMethod === 'equal'
? allMembers.filter(id => selectedMembers[id])
: allMembers;
let splits = [];
let splitType = splitMethod;
if (splitMethod === 'equal') {
const count = participants.length || 1;
const perHead = Math.floor((numericAmount / count) * 100) / 100;
let centsRemaining = Math.round(numericAmount * 100) - Math.round(perHead * 100) * count;
splits = participants.map((userId) => {
const addCent = centsRemaining-- > 0 ? 0.01 : 0;
return { userId, amount: perHead + addCent, type: 'equal' };
});
splitType = 'equal';
} else if (splitMethod === 'percentage') {
const totalPct = participants.reduce((sum, id) => sum + (parseFloat(percentages[id] || '0')), 0) || 100;
splits = participants.map((userId) => {
const pct = parseFloat(percentages[userId] || '0');
const amt = Math.round(((pct / totalPct) * numericAmount) * 100) / 100;
return { userId, amount: amt, type: 'percentage' };
});
splitType = 'percentage';
} else if (splitMethod === 'shares') {
const totalShares = participants.reduce((sum, id) => sum + (parseFloat(shares[id] || '0')), 0) || 1;
splits = participants.map((userId) => {
const s = parseFloat(shares[userId] || '0');
const amt = Math.round(((s / totalShares) * numericAmount) * 100) / 100;
return { userId, amount: amt, type: 'unequal' };
});
splitType = 'unequal';
} else if (splitMethod === 'exact') {
splits = participants.map((userId) => ({
userId,
amount: Math.round(parseFloat(exactAmounts[userId] || '0') * 100) / 100,
type: 'unequal',
}));
splitType = 'unequal';
}
const expenseData = {
description,
amount: numericAmount,
paidBy: payerId,
splitType,
splits,
};
🤖 Prompt for AI Agents
In frontend/screens/ModernAddExpenseScreen.js around lines 145 to 156, the
payload currently sends splitMethod plus raw fields (selectedMembers,
percentages, shares, exactAmounts) which likely doesn't match the backend
contract; change the code to build splitType and splits the same way the legacy
AddExpenseScreen did (derive splitType as "equal" | "unequal" | "percentage" and
build a splits array of { memberId, amount or percentage or share } entries),
then send expenseData with splitType and splits instead of splitMethod and the
raw variant fields; ensure the constructed splits follow the backend field
names/types and perform any lightweight validation (e.g. percentages sum or
member list present) before calling createExpense.

Comment on lines +89 to +95
const cardStyle = {
borderRadius: borderRadius.lg,
padding: spacing.lg,
...getVariantStyle(),
...style,
};

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

Style merging bug: spreading style into an object breaks array styles

Spreading ...style assumes an object; if callers pass an array (common in RN), overrides are lost. Compose styles as arrays to preserve precedence.

Apply this diff:

-  const cardStyle = {
-    borderRadius: borderRadius.lg,
-    padding: spacing.lg,
-    ...getVariantStyle(),
-    ...style,
-  };
+  const baseStyle = {
+    borderRadius: borderRadius.lg,
+    padding: spacing.lg,
+  };
+  const cardStyle = [baseStyle, getVariantStyle(), style];
@@
-      <Animated.View style={{ transform: [{ scale: scaleValue }] }}>
+      <Animated.View style={{ transform: [{ scale: scaleValue }] }}>
         <TouchableOpacity
           onPress={handlePress}
           onPressIn={handlePressIn}
           onPressOut={handlePressOut}
           activeOpacity={1}
-          style={cardStyle}
+          style={cardStyle}
           {...props}
         >
           {children}
         </TouchableOpacity>
       </Animated.View>
@@
-  return (
-    <View style={cardStyle} {...props}>
-      {children}
-    </View>
-  );
+  return (
+    <View style={cardStyle} {...props}>
+      {children}
+    </View>
+  );

Also applies to: 96-111, 113-118

🤖 Prompt for AI Agents
In frontend/utils/cards.js around lines 89-95 (and also apply same change for
96-111 and 113-118), the current code spreads ...style into a single object
which breaks when callers pass an array of styles (React Native common pattern)
and loses precedence; instead build the final style as an array that preserves
order and supports both object and array inputs — create a base style object
with borderRadius and padding, append getVariantStyle(), then append style but
expand it if it's an array (e.g. concat or spread Array.isArray(style) ? style :
[style]); update the other listed blocks the same way so styles are composed as
arrays rather than shallow-merged objects.

Comment on lines +263 to +270
colors={[colors.brand.accent, colors.brand.accentAlt]}
style={{
borderRadius: borderRadius.lg,
padding: spacing.lg,
...shadows.medium,
...style,
}}
start={{ x: 0, y: 0 }}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid object-spread of style on LinearGradient; compose as array

Same array-vs-object issue as above. This ensures caller styles are respected.

-      <LinearGradient
-        colors={[colors.brand.accent, colors.brand.accentAlt]}
-        style={{
-          borderRadius: borderRadius.lg,
-          padding: spacing.lg,
-          ...shadows.medium,
-          ...style,
-        }}
+      <LinearGradient
+        colors={[colors.brand.accent, colors.brand.accentAlt]}
+        style={[
+          {
+            borderRadius: borderRadius.lg,
+            padding: spacing.lg,
+            ...shadows.medium,
+          },
+          style,
+        ]}
📝 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
colors={[colors.brand.accent, colors.brand.accentAlt]}
style={{
borderRadius: borderRadius.lg,
padding: spacing.lg,
...shadows.medium,
...style,
}}
start={{ x: 0, y: 0 }}
colors={[colors.brand.accent, colors.brand.accentAlt]}
style={[
{
borderRadius: borderRadius.lg,
padding: spacing.lg,
...shadows.medium,
},
style,
]}
start={{ x: 0, y: 0 }}
🤖 Prompt for AI Agents
In frontend/utils/cards.js around lines 263 to 270, the LinearGradient uses
object-spread to merge caller-provided style into the inline style object which
prevents proper style precedence; change the style prop to an array that
composes the local style object first and then the incoming style (so caller
styles override), e.g. create a single local style object with borderRadius,
padding and shadows and pass [localStyle, style] to the LinearGradient's style
prop instead of spreading ...style into the object.

Comment on lines +399 to +411
<GlassCard
onPress={onPress}
style={{
backgroundColor: config.backgroundColor,
borderWidth: 1,
borderColor: config.borderColor,
alignItems: 'center',
padding: spacing.lg,
minHeight: 120,
justifyContent: 'center',
...style,
}}
>
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Compose QuickActionCard styles as arrays to preserve overrides

Don’t spread user style into an object.

-    <GlassCard
-      onPress={onPress}
-      style={{
-        backgroundColor: config.backgroundColor,
-        borderWidth: 1,
-        borderColor: config.borderColor,
-        alignItems: 'center',
-        padding: spacing.lg,
-        minHeight: 120,
-        justifyContent: 'center',
-        ...style,
-      }}
-    >
+    <GlassCard
+      onPress={onPress}
+      style={[
+        {
+          backgroundColor: config.backgroundColor,
+          borderWidth: 1,
+          borderColor: config.borderColor,
+          alignItems: 'center',
+          padding: spacing.lg,
+          minHeight: 120,
+          justifyContent: 'center',
+        },
+        style,
+      ]}
+    >
📝 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
<GlassCard
onPress={onPress}
style={{
backgroundColor: config.backgroundColor,
borderWidth: 1,
borderColor: config.borderColor,
alignItems: 'center',
padding: spacing.lg,
minHeight: 120,
justifyContent: 'center',
...style,
}}
>
<GlassCard
onPress={onPress}
style={[
{
backgroundColor: config.backgroundColor,
borderWidth: 1,
borderColor: config.borderColor,
alignItems: 'center',
padding: spacing.lg,
minHeight: 120,
justifyContent: 'center',
},
style,
]}
>
🤖 Prompt for AI Agents
In frontend/utils/cards.js around lines 399 to 411, the QuickActionCard styles
are created by spreading the incoming style into the inline style object which
can unintentionally override user-specified values or be lost; replace the
object spread with React Native array-style composition so the base style is the
first element and the incoming style is the second (e.g., style={[baseStyle,
style]}), ensuring you handle null/undefined style safely so user overrides
apply correctly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (5)
frontend/utils/cards.js (4)

312-317: LinearGradient style: use array composition, not object spread

Preserves caller overrides and supports array styles.

-        style={{
-          borderRadius: borderRadius.lg,
-          padding: spacing.lg,
-          ...shadows.medium,
-          ...style,
-        }}
+        style={[
+          {
+            borderRadius: borderRadius.lg,
+            padding: spacing.lg,
+            ...shadows.medium,
+          },
+          style,
+        ]}

446-458: QuickActionCard: compose styles as arrays to preserve overrides

Same array vs. object-spread issue.

-    <GlassCard
-      onPress={onPress}
-      style={{
-        backgroundColor: config.backgroundColor,
-        borderWidth: 1,
-        borderColor: config.borderColor,
-        alignItems: 'center',
-        padding: spacing.lg,
-        minHeight: 120,
-        justifyContent: 'center',
-        ...style,
-      }}
-    >
+    <GlassCard
+      onPress={onPress}
+      style={[
+        {
+          backgroundColor: config.backgroundColor,
+          borderWidth: 1,
+          borderColor: config.borderColor,
+          alignItems: 'center',
+          padding: spacing.lg,
+          minHeight: 120,
+          justifyContent: 'center',
+        },
+        style,
+      ]}
+    >

4-20: Use shared currency formatter and fix inconsistent currency symbols

Leverage the existing formatter and avoid local ad-hoc currency strings (₹/$/toFixed). This also aligns amounts across the app and prior feedback.

Apply:

@@
 import * as Haptics from 'expo-haptics';
 import { LinearGradient } from 'expo-linear-gradient';
 import { useRef } from 'react';
@@
 } from 'react-native';
 import {
   borderRadius,
   colors,
   shadows,
   spacing,
   typography
 } from './theme';
+import { formatCurrency } from './currency';
@@
-  const formatCurrency = (amount) => `₹${Math.abs(amount).toFixed(2)}`;
   const formatDate = (date) => {
@@
-          ₹{amount.toFixed(2)}
+          {formatCurrency(amount)}
@@
-              ${totalExpenses}
+              {formatCurrency(totalExpenses)}
@@
-            ${Math.abs(yourBalance).toFixed(2)}
+            {formatCurrency(Math.abs(yourBalance))}
@@
-      return {
-        text: `💰 You're owed ${settlementStatus.netBalance}`,
-        type: 'success',
-        icon: '💰'
-      };
+      return {
+        text: `💰 You're owed ${formatCurrency(settlementStatus.netBalance)}`,
+        type: 'success',
+        icon: '💰'
+      };
@@
-      return {
-        text: `💳 You owe ${Math.abs(settlementStatus.netBalance)}`,
-        type: 'warning',
-        icon: '💳'
-      };
+      return {
+        text: `💳 You owe ${formatCurrency(Math.abs(settlementStatus.netBalance))}`,
+        type: 'warning',
+        icon: '💳'
+      };

Also applies to: 162-174, 194-201, 349-351, 409-410, 527-536


89-95: Compose styles as arrays; avoid spreading user style into objects

Spreading ...style breaks when callers pass arrays and loses precedence. Use RN’s array style composition.

-  const cardStyle = {
-    borderRadius: borderRadius.lg,
-    padding: spacing.lg,
-    ...getVariantStyle(),
-    ...style,
-  };
+  const baseStyle = {
+    borderRadius: borderRadius.lg,
+    padding: spacing.lg,
+  };
+  const cardStyle = [baseStyle, getVariantStyle(), style];
frontend/screens/HomeScreen.js (1)

568-571: Avoid RN ‘gap’; use margins for spacing (quick actions, groups list, modal actions)

gap isn’t consistently supported in RN; apply spacing via margins to ensure cross-platform layout.

Styles:

   quickActionsGrid: {
     flexDirection: 'row',
     flexWrap: 'wrap',
-    gap: spacing.md,
   },
@@
-  groupsList: {
-    gap: spacing.md,
-  },
+  groupsList: {
+    // spacing handled by groupItemContainer marginBottom
+  },
@@
   modalActions: {
     flexDirection: 'row',
-    gap: spacing.md,
   },

Markup adjustments:

-        <QuickActionCard
+        <QuickActionCard
           title="Create Group"
@@
-          style={styles.quickActionCard}
+          style={[styles.quickActionCard, { marginRight: spacing.md, marginBottom: spacing.md }]}
         />
-        <QuickActionCard
+        <QuickActionCard
           title="Join Group"
@@
-          style={styles.quickActionCard}
+          style={[styles.quickActionCard, { marginBottom: spacing.md }]}
         />
-        <QuickActionCard
+        <QuickActionCard
           title="Add Friends"
@@
-          style={styles.quickActionCard}
+          style={[styles.quickActionCard, { marginRight: spacing.md }]}
         />
-        <QuickActionCard
+        <QuickActionCard
           title="My Profile"
@@
-          style={styles.quickActionCard}
+          style={styles.quickActionCard}
         />

Modal actions (add spacing between buttons by giving the first a right margin):

-              <Button
+              <Button
                 title="Cancel"
                 onPress={hideModal}
                 variant="ghost"
-                style={styles.modalActionButton}
+                style={[styles.modalActionButton, { marginRight: spacing.md }]}
               />

Also applies to: 604-605, 635-637, 221-252, 449-456

🧹 Nitpick comments (6)
frontend/utils/cards.js (2)

22-23: Remove unused screenWidth (and Dimensions import if unused elsewhere)

Dead code adds noise and confuses readers.

-const { width: screenWidth } = Dimensions.get('window');
+// removed unused screenWidth

Also remove Dimensions from the react-native import at the top if nothing else uses it in this file.


25-32: Drop unused glassEffect prop or implement it

The prop isn’t used. Either wire it (e.g., conditionally apply glass variant) or remove to reduce API surface.

frontend/screens/ModernGroupDetailsScreen.js (2)

385-388: Avoid RN ‘gap’; emulate spacing via margins

gap isn’t reliably supported across RN versions. Remove it and add marginRight on the first stat card.

-  quickStats: {
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    gap: theme.spacing.md,
-  },
+  quickStats: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+  },
-          <GlassCard variant="outlined" style={styles.statCard}>
+          <GlassCard variant="outlined" style={[styles.statCard, { marginRight: theme.spacing.md }]}>
@@
-          <GlassCard variant="outlined" style={styles.statCard}>
+          <GlassCard variant="outlined" style={styles.statCard}>

Also applies to: 151-159, 161-169


24-25: Remove unused Dimensions destructure (width/height)

Neither width nor height is used. Clean up to avoid dead code.

-const { width, height } = Dimensions.get('window');
+// removed unused Dimensions destructure
frontend/screens/HomeScreen.js (2)

113-113: Remove debug log

Avoid noisy logs in production.

-      console.log('Groups API Response:', response); // Debug log
+      // removed debug log

34-35: Remove unused height from Dimensions

height isn’t used.

-const { width, height } = Dimensions.get('window');
+const { width } = Dimensions.get('window');
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 72fea4d and 66dd38f.

📒 Files selected for processing (5)
  • frontend/navigation/GroupsStackNavigator.js (1 hunks)
  • frontend/navigation/MainNavigator.js (3 hunks)
  • frontend/screens/HomeScreen.js (1 hunks)
  • frontend/screens/ModernGroupDetailsScreen.js (1 hunks)
  • frontend/utils/cards.js (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
frontend/navigation/GroupsStackNavigator.js (3)
frontend/screens/ModernGroupDetailsScreen.js (1)
  • ModernGroupDetailsScreen (26-328)
frontend/screens/ModernAddExpenseScreen.js (1)
  • ModernAddExpenseScreen (28-400)
frontend/screens/GroupDetailsScreen.js (5)
  • route (18-214)
  • Card (180-191)
  • navigation (210-210)
  • navigation (53-66)
  • navigation (59-59)
frontend/navigation/MainNavigator.js (2)
frontend/components/navigation/ModernNavigation.js (1)
  • ModernTabBar (22-187)
frontend/navigation/GroupsStackNavigator.js (1)
  • GroupsStackNavigator (10-20)
frontend/screens/HomeScreen.js (6)
frontend/api/groups.js (6)
  • getOptimizedSettlements (5-6)
  • getOptimizedSettlements (5-6)
  • getGroups (3-3)
  • getGroups (3-3)
  • createGroup (21-21)
  • createGroup (21-21)
frontend/utils/cards.js (3)
  • QuickActionCard (418-495)
  • GroupSummaryCard (296-415)
  • GlassCard (25-118)
frontend/utils/theme.js (8)
  • colors (4-48)
  • colors (4-48)
  • spacing (88-95)
  • spacing (88-95)
  • borderRadius (97-102)
  • borderRadius (97-102)
  • typography (105-169)
  • typography (105-169)
frontend/components/navigation/ModernNavigation.js (2)
  • ModernHeader (190-311)
  • FloatingActionButton (398-459)
frontend/components/core/Button.js (1)
  • Button (9-179)
frontend/components/core/Input.js (1)
  • EnhancedTextInput (15-221)
🔇 Additional comments (2)
frontend/navigation/GroupsStackNavigator.js (1)

5-7: LGTM: routes updated to modern screens

GroupDetails and AddExpense point to the modern screens with headers hidden. GroupSettings remains accessible. Names/params align with usages in HomeScreen/ModernGroupDetailsScreen.

frontend/navigation/MainNavigator.js (1)

12-18: Custom tab bar integration looks correct

Hiding the default bar and supplying ModernTabBar via tabBar prop is standard. Labels/icons configured per tab.

Text,
TextInput,
} from "react-native-paper";
import { createGroup, getGroups, getOptimizedSettlements } from "../api/groups";
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use shared currency formatter

For consistency across components and locales.

-import { createGroup, getGroups, getOptimizedSettlements } from "../api/groups";
+import { createGroup, getGroups, getOptimizedSettlements } from "../api/groups";
+import { formatCurrency } from "../utils/currency";
📝 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
import { createGroup, getGroups, getOptimizedSettlements } from "../api/groups";
import { createGroup, getGroups, getOptimizedSettlements } from "../api/groups";
import { formatCurrency } from "../utils/currency";
🤖 Prompt for AI Agents
In frontend/screens/HomeScreen.js around line 24, the file currently imports
group API functions but does not use the shared currency formatter; import the
shared currency formatting utility from the app's common/utils (e.g., the
central formatters module) and replace any local or inline currency formatting
in this component with that shared format function, passing the numeric amount
and currency code (or app locale) so formatting is consistent across components;
update any props/variables to provide the required arguments and run a quick UI
check to ensure displayed amounts render correctly.

Comment on lines +356 to +374
</Text>
<Text style={styles.balanceLabel}>
{overallBalance.net >= 0 ? 'You are owed' : 'You owe'}
</Text>
</Animated.View>

<View style={styles.balanceBreakdown}>
<View style={styles.balanceItem}>
<Text style={styles.balanceItemAmount}>
+${overallBalance.totalToReceive.toFixed(2)}
</Text>
<Text style={styles.balanceItemLabel}>To receive</Text>
</View>
<View style={styles.balanceDivider} />
<View style={styles.balanceItem}>
<Text style={styles.balanceItemAmount}>
-${overallBalance.totalOwed.toFixed(2)}
</Text>
<Text style={styles.balanceItemLabel}>To pay</Text>
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Format currency using shared utility; drop raw $ and toFixed

Keeps amounts consistent with other screens and cards.

-                {overallBalance.net >= 0 ? '+' : ''}${Math.abs(overallBalance.net).toFixed(2)}
+                {overallBalance.net >= 0 ? '+' : ''}{formatCurrency(Math.abs(overallBalance.net))}
@@
-                  +${overallBalance.totalToReceive.toFixed(2)}
+                  +{formatCurrency(overallBalance.totalToReceive)}
@@
-                  -${overallBalance.totalOwed.toFixed(2)}
+                  -{formatCurrency(overallBalance.totalOwed)}

Also applies to: 366-367, 373-374

🤖 Prompt for AI Agents
In frontend/screens/HomeScreen.js around lines 356 to 374 (also applies to
366-367 and 373-374), the component is constructing currency strings manually
using raw "$" and Number.toFixed, causing inconsistent formatting; replace those
manual constructions with the shared currency utility (e.g., formatCurrency or
whatever the project exports) for all displayed amounts (overallBalance.net,
overallBalance.totalToReceive, overallBalance.totalOwed), remove the hardcoded
"$" and toFixed calls, and pass the numeric values into the shared formatter so
output matches other screens.

import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as Haptics from 'expo-haptics';
import { LinearGradient } from 'expo-linear-gradient';
import { useContext, useEffect, useState } from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Persist Animated values with useRef

Animated.Value instances are recreated on each render, breaking animations. Import useRef.

-import { useContext, useEffect, useState } from 'react';
+import { useContext, useEffect, useRef, useState } from 'react';
📝 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
import { useContext, useEffect, useState } from 'react';
import { useContext, useEffect, useRef, useState } from 'react';
🤖 Prompt for AI Agents
In frontend/screens/ModernGroupDetailsScreen.js around line 4, Animated.Value
instances are being recreated each render because useRef isn't imported or used;
import useRef from React on the same import line and create/persist
Animated.Value (e.g., const anim = useRef(new Animated.Value(0)).current)
instead of constructing them inside render or useState so the Animated.Value
survives re-renders; update any code that references the previous state-based
Animated values to use the ref-backed .current value.

Comment on lines +35 to +39
// Animated values
const scrollY = new Animated.Value(0);
const fadeAnim = new Animated.Value(0);
const scaleAnim = new Animated.Value(0.9);

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix: Recreate-proof Animated values

Use refs so values persist across renders.

-  const scrollY = new Animated.Value(0);
-  const fadeAnim = new Animated.Value(0);
-  const scaleAnim = new Animated.Value(0.9);
+  const scrollY = useRef(new Animated.Value(0)).current;
+  const fadeAnim = useRef(new Animated.Value(0)).current;
+  const scaleAnim = useRef(new Animated.Value(0.9)).current;
📝 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
// Animated values
const scrollY = new Animated.Value(0);
const fadeAnim = new Animated.Value(0);
const scaleAnim = new Animated.Value(0.9);
// Animated values
const scrollY = useRef(new Animated.Value(0)).current;
const fadeAnim = useRef(new Animated.Value(0)).current;
const scaleAnim = useRef(new Animated.Value(0.9)).current;
🤖 Prompt for AI Agents
In frontend/screens/ModernGroupDetailsScreen.js around lines 35 to 39, the
Animated.Value instances are created directly in the render path which recreates
them on every render; change them to use refs (e.g., useRef or createRef) so
scrollY, fadeAnim and scaleAnim are initialized once and persist across renders;
replace the direct new Animated.Value(...) calls with ref-initialized values
(accessing .current where used) to ensure animation state is stable.

Comment on lines +270 to +280
<Animated.ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
{ useNativeDriver: false }
)}
refreshing={refreshing}
onRefresh={handleRefresh}
>
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

Fix pull-to-refresh: use RefreshControl instead of unsupported props

Animated.ScrollView doesn’t accept refreshing/onRefresh. Attach a RefreshControl.

-        <Animated.ScrollView
+        <Animated.ScrollView
           style={styles.scrollView}
           contentContainerStyle={styles.scrollContent}
           showsVerticalScrollIndicator={false}
           onScroll={Animated.event(
             [{ nativeEvent: { contentOffset: { y: scrollY } } }],
             { useNativeDriver: false }
           )}
-          refreshing={refreshing}
-          onRefresh={handleRefresh}
+          refreshControl={
+            <RefreshControl
+              refreshing={refreshing}
+              onRefresh={handleRefresh}
+            />
+          }
         >

Also add RefreshControl to the react-native import list at the top.

-    Alert,
-    Animated,
-    Dimensions,
-    StyleSheet,
-    Text,
-    View
+    Alert,
+    Animated,
+    Dimensions,
+    RefreshControl,
+    StyleSheet,
+    Text,
+    View
📝 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
<Animated.ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
{ useNativeDriver: false }
)}
refreshing={refreshing}
onRefresh={handleRefresh}
>
import {
Alert,
Animated,
Dimensions,
RefreshControl,
StyleSheet,
Text,
View
} from 'react-native';
<Animated.ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
{ useNativeDriver: false }
)}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
/>
}
>
🤖 Prompt for AI Agents
In frontend/screens/ModernGroupDetailsScreen.js around lines 270 to 280,
Animated.ScrollView is incorrectly given refreshing and onRefresh props (which
it doesn't support); replace those props by passing a RefreshControl instance
via the refreshControl prop (e.g. refreshControl={<RefreshControl
refreshing={refreshing} onRefresh={handleRefresh} />}), and add RefreshControl
to the react-native import list at the top of the file.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (9)
frontend/components/core/Button.js (2)

93-107: Avoid passing non-style keys into style objects

currentVariant is spread into buttonStyle, leaking non-style keys (useGradient, gradientColors, textColor). Extract style keys first, then spread.

Apply this diff:

   const currentSize = sizeConfig[size];
   const currentVariant = variantConfig[variant];
 
+  // Extract non-style keys to avoid polluting style objects
+  const { useGradient, gradientColors, textColor, shadowStyle, ...variantStyle } = currentVariant || {};
+
   // Base button style
   const buttonStyle = {
     borderRadius: borderRadius.md,
     alignItems: 'center',
     justifyContent: 'center',
     flexDirection: 'row',
     minHeight: currentSize.minHeight,
     paddingVertical: currentSize.paddingVertical,
     paddingHorizontal: currentSize.paddingHorizontal,
     width: fullWidth ? '100%' : 'auto',
     opacity: disabled ? 0.6 : 1,
-    ...currentVariant.shadowStyle,
-    ...currentVariant,
+    ...(shadowStyle || {}),
+    ...variantStyle,
     ...style,
   };

141-165: Disabled primary loses background — remove disabled gate for gradient

The gradient path is skipped when disabled, leading to transparent background for disabled primary buttons. Keep gradient regardless; rely on opacity for affordance.

Apply this minimal diff:

-  // Render with gradient if specified
-  if (currentVariant.useGradient && !disabled) {
+  // Render with gradient if specified (even when disabled; opacity handles affordance)
+  if (currentVariant.useGradient) {
frontend/utils/cards.js (7)

89-94: Fix style merging issue for array styles

Spreading ...style into an object breaks when callers pass style arrays (common in React Native). Compose styles as arrays to preserve precedence.

-  const cardStyle = {
-    borderRadius: borderRadius.lg,
-    padding: spacing.lg,
-    ...getVariantStyle(),
-    ...style,
-  };
+  const baseStyle = {
+    borderRadius: borderRadius.lg,
+    padding: spacing.lg,
+  };
+  const cardStyle = [baseStyle, getVariantStyle(), style];

162-162: Import and use the existing formatCurrency utility

The project already has a formatCurrency function in frontend/utils/currency.js. Import and use it for consistency across the app.

Add the import at the top of the file:

+import { formatCurrency } from './currency';

Then remove the local formatCurrency function on line 162 and update the usage to match the imported function's signature.


200-200: Use formatCurrency for expense amount display

For consistency, use the formatCurrency utility instead of manual formatting.

-          ₹{amount.toFixed(2)}
+          {formatCurrency(amount)}

221-223: Apply formatCurrency to user position amounts

Use the shared formatter for consistency.

-            {isOwed 
-              ? `💰 You're owed ${formatCurrency(netAmount)}`
-              : `💳 You owe ${formatCurrency(netAmount)}`
+            {isOwed 
+              ? `💰 You're owed ${formatCurrency(netAmount)}`
+              : `💳 You owe ${formatCurrency(Math.abs(netAmount))}`

Note: After importing the shared formatCurrency, ensure it handles negative values appropriately or use Math.abs() as shown.


313-317: Compose LinearGradient styles as array to handle caller overrides

Same style merging issue as the base card - use array composition.

-        style={{
-          borderRadius: borderRadius.lg,
-          padding: spacing.lg,
-          ...shadows.medium,
-          ...style,
-        }}
+        style={[
+          {
+            borderRadius: borderRadius.lg,
+            padding: spacing.lg,
+            ...shadows.medium,
+          },
+          style,
+        ]}

449-458: Fix style composition in QuickActionCard

Apply array-based style composition to preserve caller overrides.

-      style={{
-        backgroundColor: config.backgroundColor,
-        borderWidth: 1,
-        borderColor: config.borderColor,
-        alignItems: 'center',
-        padding: spacing.lg,
-        minHeight: 120,
-        justifyContent: 'center',
-        ...style,
-      }}
+      style={[
+        {
+          backgroundColor: config.backgroundColor,
+          borderWidth: 1,
+          borderColor: config.borderColor,
+          alignItems: 'center',
+          padding: spacing.lg,
+          minHeight: 120,
+          justifyContent: 'center',
+        },
+        style,
+      ]}

527-538: Format currency in settlementStatus display

Apply consistent currency formatting to balance amounts.

     if (settlementStatus.netBalance > 0) {
       return {
-        text: `💰 You're owed ${settlementStatus.netBalance}`,
+        text: `💰 You're owed ${formatCurrency(settlementStatus.netBalance)}`,
         type: 'success',
         icon: '💰'
       };
     } else if (settlementStatus.netBalance < 0) {
       return {
-        text: `💳 You owe ${Math.abs(settlementStatus.netBalance)}`,
+        text: `💳 You owe ${formatCurrency(Math.abs(settlementStatus.netBalance))}`,
         type: 'warning',
         icon: '💳'
       };
     }
🧹 Nitpick comments (11)
frontend/components/core/Button.js (5)

22-31: Don't block onPress if haptics fails; guard with try/finally

Ensure the action fires even if the haptic call throws or is unavailable. This also isolates UX issues from core behavior.

Apply this diff:

 const handlePress = async () => {
   if (disabled || loading) return;
-  
-  // Haptic feedback for engagement (Gen Z preference for tactile response)
-  await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
-  
-  if (onPress) {
-    onPress();
-  }
+  try {
+    // Haptic feedback for engagement (Gen Z preference for tactile response)
+    await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
+  } catch {
+    // no-op: do not block user action if haptics is unavailable
+  } finally {
+    onPress?.();
+  }
 };

35-41: Small size violates 44px minimum touch target

Your comment says "minimum touch target of 44px", but small.minHeight is 36. Consider bumping to 44 to meet accessibility guidance.

If acceptable, apply:

   small: {
     paddingVertical: spacing.sm,
     paddingHorizontal: spacing.md,
-    minHeight: 36,
+    minHeight: 44,
     fontSize: 14,
     fontWeight: '500',
   },

109-120: Align text and spinner color usage with extracted variant fields

If you apply the variant destructuring suggested above, use the extracted textColor for consistency.

Apply this diff (only if you applied the previous refactor):

   const textStyleConfig = {
     fontSize: currentSize.fontSize,
     fontWeight: currentSize.fontWeight,
-    color: currentVariant.textColor,
+    color: textColor,
     fontFamily: 'Inter',
     ...textStyle,
   };
 
   // Loading spinner color
-  const spinnerColor = currentVariant.textColor;
+  const spinnerColor = textColor;

144-158: Fix double padding in gradient branch (outer + inner styles)

Both the outer Touchable and inner LinearGradient apply buttonStyle (which contains padding), doubling vertical/horizontal padding. Zero out padding on the outer wrapper so only the inner container controls content layout.

Apply this diff:

       <TouchableOpacity
         onPress={handlePress}
         disabled={disabled || loading}
         activeOpacity={0.8}
-        style={[buttonStyle, { backgroundColor: 'transparent' }]}
+        style={[
+          buttonStyle,
+          { backgroundColor: 'transparent', paddingVertical: 0, paddingHorizontal: 0 }
+        ]}
         {...props}
       >
         <LinearGradient
-          colors={currentVariant.gradientColors}
+          colors={currentVariant.gradientColors}
           style={{
             ...buttonStyle,
             shadowColor: 'transparent', // Remove shadow from gradient container
             elevation: 0,
           }}

144-151: Add accessibility metadata to Touchable

Expose role and state for screen readers; set disabled/busy and label from title.

Apply these diffs:

@@
-      <TouchableOpacity
+      <TouchableOpacity
         onPress={handlePress}
         disabled={disabled || loading}
         activeOpacity={0.8}
         style={[
           buttonStyle,
           { backgroundColor: 'transparent', paddingVertical: 0, paddingHorizontal: 0 }
         ]}
+        accessibilityRole="button"
+        accessibilityState={{ disabled: disabled || loading, busy: !!loading }}
+        accessibilityLabel={typeof title === 'string' ? title : undefined}
         {...props}
       >
@@
   return (
     <TouchableOpacity
       onPress={handlePress}
       disabled={disabled || loading}
       activeOpacity={0.8}
       style={buttonStyle}
+      accessibilityRole="button"
+      accessibilityState={{ disabled: disabled || loading, busy: !!loading }}
+      accessibilityLabel={typeof title === 'string' ? title : undefined}
       {...props}
     >

Also applies to: 169-175

frontend/utils/cards.js (2)

22-22: Remove unused screenWidth variable

The screenWidth variable is computed but never used in the code.

-const { width: screenWidth } = Dimensions.get('window');

582-586: Fix inconsistent export formatting

The export statement has an awkward line break in the middle of the destructured exports.

 export {
-  ExpenseCard, GlassCard, GroupCard // For backward compatibility
-  ,
+  ExpenseCard, 
+  GlassCard, 
+  GroupCard, // For backward compatibility
   GroupSummaryCard,
   QuickActionCard
 };
frontend/components/navigation/ModernNavigation.js (4)

19-19: Consider extracting screenWidth to avoid recalculation.

The screenWidth is calculated at module load time, which is efficient. However, consider that this won't update if the device orientation changes during runtime.

If orientation changes are expected, consider moving this inside the component or using a hook:

-const { width: screenWidth } = Dimensions.get('window');
+// Move inside FloatingActionButton component or create a custom hook

69-84: Consider using icon utilities for consistency.

The hardcoded emoji icons work but may not be consistent with the icon system referenced in the AI summary. Consider using the icon utilities from frontend/utils/icons.js for better consistency.

Based on the context, there's an icon utility available:

// Import the icon utility
+import { getIcon } from '../../utils/icons';

// Update the getTabConfig function
const getTabConfig = (routeName) => {
  switch (routeName) {
    case 'Home':
-     return { icon: '🏠', label: 'Home' };
+     return { icon: getIcon('home'), label: 'Home' };
    case 'Groups':
-     return { icon: '👥', label: 'Groups' };
+     return { icon: getIcon('groups'), label: 'Groups' };
    // ... continue for other cases
  }
};

244-259: Consider using a proper icon for the back button.

The back button uses a simple arrow character which may not render consistently across platforms.

Consider using a proper icon component or the icon utility:

-<Text style={{ fontSize: 20 }}>←</Text>
+<Text style={{ fontSize: 20 }}>{getIcon('back') || '←'}</Text>

461-464: Fix export formatting for better readability.

The export statement has inconsistent formatting that makes it harder to read.

export {
    ActionButton,
-   FloatingActionButton, ModernHeader, ModernTabBar
+   FloatingActionButton,
+   ModernHeader,
+   ModernTabBar
};
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 66dd38f and 5ffd608.

📒 Files selected for processing (6)
  • frontend/components/core/Button.js (1 hunks)
  • frontend/components/core/Input.js (1 hunks)
  • frontend/components/navigation/ModernNavigation.js (1 hunks)
  • frontend/screens/HomeScreen.js (1 hunks)
  • frontend/utils/cards.js (1 hunks)
  • frontend/utils/theme.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/screens/HomeScreen.js
  • frontend/utils/theme.js
  • frontend/components/core/Input.js
🧰 Additional context used
🧬 Code Graph Analysis (3)
frontend/components/navigation/ModernNavigation.js (4)
frontend/utils/cards.js (3)
  • screenWidth (22-22)
  • handlePress (53-58)
  • handlePress (505-508)
frontend/screens/ModernAddExpenseScreen.js (1)
  • screenWidth (26-26)
frontend/utils/theme.js (8)
  • colors (4-48)
  • colors (4-48)
  • spacing (88-95)
  • spacing (88-95)
  • shadows (172-213)
  • shadows (172-213)
  • typography (105-169)
  • typography (105-169)
frontend/components/core/Button.js (1)
  • handlePress (22-31)
frontend/components/core/Button.js (3)
frontend/utils/cards.js (2)
  • handlePress (53-58)
  • handlePress (505-508)
frontend/components/navigation/ModernNavigation.js (2)
  • handlePress (322-325)
  • handlePress (404-407)
frontend/utils/theme.js (8)
  • spacing (88-95)
  • spacing (88-95)
  • colors (4-48)
  • colors (4-48)
  • shadows (172-213)
  • shadows (172-213)
  • borderRadius (97-102)
  • borderRadius (97-102)
frontend/utils/cards.js (4)
frontend/components/core/Button.js (1)
  • handlePress (22-31)
frontend/utils/theme.js (10)
  • colors (4-48)
  • colors (4-48)
  • shadows (172-213)
  • shadows (172-213)
  • borderRadius (97-102)
  • borderRadius (97-102)
  • spacing (88-95)
  • spacing (88-95)
  • typography (105-169)
  • typography (105-169)
frontend/components/core/Input.js (1)
  • formatCurrency (233-249)
frontend/utils/icons.js (1)
  • config (128-128)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: bundle-analysis
  • GitHub Check: preview
🔇 Additional comments (12)
frontend/components/core/Button.js (1)

121-139: LGTM: Clean, reusable ButtonContent with loading and icon slots

Good separation of concerns and spacing. Spinner/text color coupling to variant color is solid.

frontend/components/navigation/ModernNavigation.js (11)

1-17: LGTM! Well-structured imports and organization.

The file is well-organized with clear imports and proper structure. The theme imports are consistent with the existing design system.


22-51: LGTM! Clean prop destructuring and label resolution logic.

The component signature is well-defined and the label resolution logic properly follows React Navigation's hierarchy (tabBarLabel > title > route.name).


55-66: LGTM! Proper haptic feedback and navigation event handling.

The async onPress handler correctly implements haptic feedback and follows React Navigation's event-driven approach with proper event prevention checks.


89-125: LGTM! Well-implemented special button with gradient styling.

The special handling for the center Add button is well-implemented with proper gradient styling and consistent dimensions. The conditional rendering is clean and the styling follows the design system.


128-182: LGTM! Comprehensive regular tab implementation.

The regular tab implementation is thorough with proper focus states, consistent styling, and nice active indicator. The opacity transitions and color changes provide clear visual feedback.


190-222: LGTM! Flexible header component with good variant system.

The ModernHeader component provides good flexibility with its variant system (default, transparent, gradient) and proper prop handling for different use cases.


298-311: LGTM! Clean gradient variant implementation.

The conditional gradient wrapping is cleanly implemented and maintains the same content structure regardless of variant.


314-395: LGTM! Well-designed ActionButton with comprehensive variant system.

The ActionButton component is well-designed with:

  • Proper size and variant configurations
  • Consistent haptic feedback
  • Support for both string and component icons
  • Good accessibility with proper touch targets

398-459: LGTM! Well-implemented FloatingActionButton with flexible positioning.

The FloatingActionButton is well-implemented with:

  • Heavy haptic feedback appropriate for primary actions
  • Flexible positioning system
  • Consistent gradient styling
  • Proper z-index for overlay positioning

1-465: Overall excellent implementation with modern UX patterns.

This navigation component module excellently implements modern mobile UX patterns with:

  • Proper haptic feedback throughout
  • Consistent gradient theming
  • Glassmorphism effects
  • Flexible component variants
  • Good accessibility considerations

The code follows React Native best practices and integrates well with the existing design system.


422-422: FAB center positioning verified — no change required

The calculation left: (screenWidth / 2) - 28 correctly centers the FAB because the FAB is defined as 56px wide (half = 28px). I confirmed the size is consistently used in the repo.

  • frontend/components/navigation/ModernNavigation.js — center calc at line 422 (left: (screenWidth / 2) - 28) and FAB style definitions at lines 95, 106, 440–442 (width: 56, height: 56, borderRadius: 28).
  • frontend/utils/skeletons.js — skeletonAvatar uses width: 56 / borderRadius: 28.
  • docs/design-system.md — Floating Action Button width documented as 56.

No code changes required.

...typography.h2,
color: '#FFFFFF',
}}>
${totalExpenses}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use consistent currency formatting in GroupSummaryCard

The component uses $ symbol while the rest of the app uses . Import and use formatCurrency for consistency.

-              ${totalExpenses}
+              {formatCurrency(totalExpenses)}
-            ${Math.abs(yourBalance).toFixed(2)}
+            {formatCurrency(Math.abs(yourBalance))}

Also applies to: 409-409

🤖 Prompt for AI Agents
In frontend/utils/cards.js around lines 349 and 409, the GroupSummaryCard is
hardcoding a `$` currency symbol instead of using the app's standard formatter;
import the existing formatCurrency helper at the top of the file and replace the
literal `$${totalExpenses}` (and the similar occurrence at line 409) with
formatCurrency(totalExpenses) so amounts render with the app's ₹ formatting and
locale-aware formatting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-preview To run Expo preview build on forks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants