-
Notifications
You must be signed in to change notification settings - Fork 24
Revamp Home and Join Group screens with modern UI and animations #147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- 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.
WalkthroughAdds 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
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 unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
Codecov Report✅ All modified and coverable lines are covered by tests. 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
🚀 New features to boost your workflow:
|
- 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.
There was a problem hiding this 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 conditionThe 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_dispatchgithub.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 runsgithub.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 immediatelyThis 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):
- 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.
- 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).
- 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 formatterThere’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 utilityNote: The imported
formatCurrencyreturns a symbol-prefixed, validated string and aligns with app-wide settings.Also applies to: 15-17
frontend/screens/GroupSettingsScreen.js (2)
137-147: Carry throughmimeTypefrom the picker and clear emoji selection when an image is chosenThe picker returns
asset.mimeType(orasset.typeon some SDKs). Store it soonSavecan 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 noiconfield, so apply MIME fix onlyThe app currently hardcodes
data:image/jpeg;base64,...for picked images (can be PNG/WebP/GIF). Backend/group API only supportsimageUrl(noiconfield), so don't change storage of emoji to a separateiconfield 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
iconfield:
- 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
iconfield, 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 previewsAvoid 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 speednpm 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 hardeningPinning 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 platformsExpo 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 defaultsThis 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 unmountYou 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 touchesSkeletons 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 roleAligns 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 sizesFor 40x40 buttons, consider adding
hitSlopfor 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 duplicationA 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 propsTouchableWithoutFeedback 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 remountsThe 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 patternsProviding 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” actionAdd 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 validCurrently 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 backgroundColorStyling
backgroundColoron Paper Button can conflict with theme and disabled/pressed states. PreferbuttonColorprop 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 subtitleIf
searchTermcontains 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
widthis declared but never used.-const { width } = Dimensions.get('window');
71-74: Defensive member name lookupIf
membersdoesn’t contain the entry or shapes vary, this can throw when accessingmember.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 duplicationYou introduced
frontend/utils/emptyStates.jswithEmptyExpenses. 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 useLayoutEffectYou call
navigation.setOptionsinside auseEffectwith[token, groupId]. To avoid stale closures and align with React Navigation’s guidance, includenavigationandgroupNamein deps or switch touseLayoutEffect.- 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
useLayoutEffectfrom React.Also applies to: 69-69
frontend/screens/HomeScreen.js (3)
21-21: Remove unused Dimensions constant
widthis declared but not used.-const { width } = Dimensions.get('window');
291-305: Leverage EmptyGroups for consistent empty state and less codeYou’ve added a reusable empty state module. Use
EmptyGroupsinstead 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 gradientPer-item delay and
StatusGradientstatus 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 dismissYou mark
visible: falsebut 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 versionShort: 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 slotWrapping an
IconButtonwithTouchableOpacitycan complicate a11y and hit slop. Paper’sTextInputsupportsright/leftprops withTextInput.Iconfor 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 casesThe 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 layoutAbsolute-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.Iconslots as suggested above to drop these styles entirely.Also applies to: 218-228
156-168: Search clear button press targetCurrently, the
rightIconis anIconButtonwithoutonPressand the wrapper handles press. To improve a11y, consider passing the handler to the icon itself and removing the wrapper or addingaccessibilityRole="button"andaccessibilityLabel="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: UsecontainerColorfor IconButton background and add an accessibility labelPaper’s IconButton supports
containerColor. Setting background viastyleis 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
removeButtonstyle if unused.
470-478: Improve accessibility for the Share buttonAdd 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/savingPrevents 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 UXYou 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
getOptimizedSettlementscheck 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.
⛔ Files ignored due to path filters (1)
frontend/package-lock.jsonis 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 remountsIndex keys are fine for static lists, but if
countchanges 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 neededConfirmed: 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-gradientversion 14.1.5frontend/App.js (1)
10-14: LGTM: Provider composition and theming integration are correctPaperProvider 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-inThe gradient hero wrapped in FadeInView reads clean and aligns with the new design system.
frontend/utils/emptyStates.js (1)
7-43: Solid, reusable EmptyState abstractionThe component covers illustration, icon, title, subtitle, and optional CTA cleanly with entry animation. Good guard on rendering the action only when both
actionTextandonActionare present.frontend/utils/toast.js (2)
24-39: Good haptics mapping per toast typeTriggering haptics per toast type is a UX win and the default to light on INFO is sensible.
115-131: Ergonomic hook APIThe hook exposes convenience helpers
showSuccess|Error|Warning|Infoand enforces provider usage — clean and safe.frontend/screens/GroupSettingsScreen.js (4)
266-313: Members list rendering looks solidGood 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 cohesiveLoader with FadeInView and the gradient header card are well-structured and align with the new design system.
455-482: Invite flow is straightforward and consistentThe 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 requiredVerified: frontend/package.json uses react-native 0.79.5 (Expo SDK ~53). React Native added
gapin 0.71 (percentage support in 0.75+), so usinggaphere 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.
| on: | ||
| pull_request_target: | ||
| workflow_dispatch: | ||
|
|
There was a problem hiding this comment.
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: numberCommittable 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.
| "expo-haptics": "^14.1.4", | ||
| "expo-image-picker": "~16.0.2", | ||
| "expo-linear-gradient": "^14.1.5", |
There was a problem hiding this comment.
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"
fiLength 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.
| 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'; |
There was a problem hiding this comment.
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.
| 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.
| gap: spacing.md, | ||
| }, |
There was a problem hiding this comment.
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.
| {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> |
There was a problem hiding this comment.
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.
| {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.
| // 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(); | ||
| }, | ||
| }; |
There was a problem hiding this comment.
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.
| // 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.
| 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} | ||
| /> | ||
| ); |
There was a problem hiding this comment.
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.
| 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.
| 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} | ||
| /> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
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.
| 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.
| keyboardType="numeric" | ||
| leftIcon={ | ||
| <Text style={styles.currencySymbol}>{currency}</Text> | ||
| } | ||
| style={style} | ||
| {...props} | ||
| /> |
There was a problem hiding this comment.
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.
| 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.
| 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)', | ||
| }, | ||
| }; |
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this 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: Replacegapin 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: Removegapfrom groupsList; rely on item marginsYou already set vertical spacing via
groupItemContainer. Dropgapto avoid platform issues.groupsList: { - gap: spacing.md, },
614-616: Modal actions: avoidgapand space buttons with marginsRN
gapisn’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 functionsAnimated/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 consistencyRaw 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 polishInsert “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 polishAdd 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 polishUse 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 containerYou’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 barAnimating 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 amountPrevent 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 usinggapin RN styles (inconsistent support)React Native’s
gaphas limited/partial support across versions and platforms. Use margins on children to achieve spacing.Example adjustments:
- Remove
gapfrom grids/lists.- Add
marginRight/marginBottomto 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 normalizationEntering “.” 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-limitingTriggering 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 groupFetching 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:memberCountfallback of 2 is arbitraryIf 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 providerApp.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 NativeThese 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 unusedDimensionsimport andscreenWidthThey’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:glassEffectprop is unusedEither 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 paddingWithout 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 insetsEnsure 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 buttonThe 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 conventionsEmitting '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.
📒 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 requiredConfirmed that frontend/utils/theme.js exports
animationsand includestiming.fast(150).
- Location: frontend/utils/theme.js —
export const animations(lines ~216–218)
| // 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, | ||
| }; |
There was a problem hiding this comment.
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.
| // 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> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| // 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).
| import { | ||
| Alert, | ||
| Animated, | ||
| Dimensions, | ||
| ScrollView, | ||
| StyleSheet, | ||
| View | ||
| } from "react-native"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| try { | ||
| const expenseData = { | ||
| description, | ||
| amount: parseFloat(amount), | ||
| paidBy: payerId, | ||
| splitMethod, | ||
| ...(splitMethod === "equal" && { selectedMembers }), | ||
| ...(splitMethod === "percentage" && { percentages }), | ||
| ...(splitMethod === "shares" && { shares }), | ||
| ...(splitMethod === "exact" && { exactAmounts }), | ||
| }; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| const cardStyle = { | ||
| borderRadius: borderRadius.lg, | ||
| padding: spacing.lg, | ||
| ...getVariantStyle(), | ||
| ...style, | ||
| }; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| colors={[colors.brand.accent, colors.brand.accentAlt]} | ||
| style={{ | ||
| borderRadius: borderRadius.lg, | ||
| padding: spacing.lg, | ||
| ...shadows.medium, | ||
| ...style, | ||
| }} | ||
| start={{ x: 0, y: 0 }} |
There was a problem hiding this comment.
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.
| 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.
| <GlassCard | ||
| onPress={onPress} | ||
| style={{ | ||
| backgroundColor: config.backgroundColor, | ||
| borderWidth: 1, | ||
| borderColor: config.borderColor, | ||
| alignItems: 'center', | ||
| padding: spacing.lg, | ||
| minHeight: 120, | ||
| justifyContent: 'center', | ||
| ...style, | ||
| }} | ||
| > |
There was a problem hiding this comment.
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.
| <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.
…ience in group management
There was a problem hiding this 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 spreadPreserves 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 overridesSame 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 symbolsLeverage 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 objectsSpreading ...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 screenWidthAlso 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 itThe 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 marginsgap 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 destructurefrontend/screens/HomeScreen.js (2)
113-113: Remove debug logAvoid noisy logs in production.
- console.log('Groups API Response:', response); // Debug log + // removed debug log
34-35: Remove unused height from Dimensionsheight 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.
📒 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 screensGroupDetails 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 correctHiding 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"; |
There was a problem hiding this comment.
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.
| 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.
| </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> |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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.
| 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.
| // Animated values | ||
| const scrollY = new Animated.Value(0); | ||
| const fadeAnim = new Animated.Value(0); | ||
| const scaleAnim = new Animated.Value(0.9); | ||
|
|
There was a problem hiding this comment.
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.
| // 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.
| <Animated.ScrollView | ||
| style={styles.scrollView} | ||
| contentContainerStyle={styles.scrollContent} | ||
| showsVerticalScrollIndicator={false} | ||
| onScroll={Animated.event( | ||
| [{ nativeEvent: { contentOffset: { y: scrollY } } }], | ||
| { useNativeDriver: false } | ||
| )} | ||
| refreshing={refreshing} | ||
| onRefresh={handleRefresh} | ||
| > |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| <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.
There was a problem hiding this 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 objectscurrentVariant 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 gradientThe 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 stylesSpreading
...styleinto 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 utilityThe project already has a
formatCurrencyfunction infrontend/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
formatCurrencyfunction on line 162 and update the usage to match the imported function's signature.
200-200: Use formatCurrency for expense amount displayFor consistency, use the formatCurrency utility instead of manual formatting.
- ₹{amount.toFixed(2)} + {formatCurrency(amount)}
221-223: Apply formatCurrency to user position amountsUse 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 useMath.abs()as shown.
313-317: Compose LinearGradient styles as array to handle caller overridesSame 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 QuickActionCardApply 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 displayApply 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/finallyEnsure 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 targetYour 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 fieldsIf 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 TouchableExpose 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 unusedscreenWidthvariableThe
screenWidthvariable is computed but never used in the code.-const { width: screenWidth } = Dimensions.get('window');
582-586: Fix inconsistent export formattingThe 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.jsfor 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.
📒 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 slotsGood 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 requiredThe calculation
left: (screenWidth / 2) - 28correctly 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 —
skeletonAvataruseswidth: 56/borderRadius: 28.- docs/design-system.md — Floating Action Button width documented as
56.No code changes required.
| ...typography.h2, | ||
| color: '#FFFFFF', | ||
| }}> | ||
| ${totalExpenses} |
There was a problem hiding this comment.
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.
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
Components
UI/UX Improvements
Dependencies
Chores