Skip to content

Commit d609f56

Browse files
Batch 4: Front-end polish, compliance, and mobile UX improvements
1 parent bea8626 commit d609f56

13 files changed

Lines changed: 475 additions & 134 deletions

File tree

apps/mobile/app/screens/AddExpenseScreen.tsx

Lines changed: 82 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -497,35 +497,37 @@ export function AddExpenseScreen({
497497
case 3:
498498
return (
499499
<Animated.View key="step3" entering={enterAnim} exiting={exitAnim} style={styles.stepContent}>
500-
<View style={styles.defaultSplitSection}>
501-
<View style={{ flex: 1 }}>
502-
<Text style={[typography.bodyLarge, { color: colors.text_primary, fontWeight: '700' }]}>Default split</Text>
503-
<Text style={[typography.bodyMedium, { color: colors.text_secondary }]}>Save the current split setup for this group.</Text>
504-
</View>
505-
<View style={styles.defaultSplitActions}>
506-
<Button variant="secondary" onPress={() => void resetDefaultSplit()} loading={savingDefault} style={styles.defaultSplitButton}>
507-
Reset
508-
</Button>
509-
<Button variant="primary" onPress={() => void saveDefaultSplit()} loading={savingDefault} style={styles.defaultSplitButton}>
510-
Save Default
511-
</Button>
500+
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: spacing.xl }}>
501+
<View style={styles.defaultSplitSection}>
502+
<View style={{ flex: 1 }}>
503+
<Text style={[typography.bodyLarge, { color: colors.text_primary, fontWeight: '700' }]}>Default split</Text>
504+
<Text style={[typography.bodyMedium, { color: colors.text_secondary }]}>Save the current split setup for this group.</Text>
505+
</View>
506+
<View style={styles.defaultSplitActions}>
507+
<Button variant="secondary" onPress={() => void resetDefaultSplit()} loading={savingDefault} style={styles.defaultSplitButton}>
508+
Reset
509+
</Button>
510+
<Button variant="primary" onPress={() => void saveDefaultSplit()} loading={savingDefault} style={styles.defaultSplitButton}>
511+
Save Default
512+
</Button>
513+
</View>
512514
</View>
513-
</View>
514-
<SplitSelector
515-
members={members}
516-
splitType={splitType}
517-
selectedParticipantIds={selectedParticipantIds}
518-
exactByUser={exactByUser}
519-
percentagesByUser={percentagesByUser}
520-
onSplitTypeChange={setSplitType}
521-
onParticipantsChange={setSelectedParticipantIds}
522-
onExactChange={(userId, value) =>
523-
setExactByUser((state) => ({ ...state, [userId]: value }))
524-
}
525-
onPercentageChange={(userId, value) =>
526-
setPercentagesByUser((state) => ({ ...state, [userId]: value }))
527-
}
528-
/>
515+
<SplitSelector
516+
members={members}
517+
splitType={splitType}
518+
selectedParticipantIds={selectedParticipantIds}
519+
exactByUser={exactByUser}
520+
percentagesByUser={percentagesByUser}
521+
onSplitTypeChange={setSplitType}
522+
onParticipantsChange={setSelectedParticipantIds}
523+
onExactChange={(userId, value) =>
524+
setExactByUser((state) => ({ ...state, [userId]: value }))
525+
}
526+
onPercentageChange={(userId, value) =>
527+
setPercentagesByUser((state) => ({ ...state, [userId]: value }))
528+
}
529+
/>
530+
</ScrollView>
529531
</Animated.View>
530532
);
531533

@@ -534,62 +536,64 @@ export function AddExpenseScreen({
534536
const shares = totalAmount > 0 ? buildShares(totalAmount) : {};
535537
return (
536538
<Animated.View key="step4" entering={enterAnim} exiting={exitAnim} style={styles.stepContent}>
537-
<Card variant="elevated" style={styles.reviewCard}>
538-
<View style={styles.reviewItem}>
539-
<Text style={[typography.caption, { color: colors.muted }]}>DESCRIPTION</Text>
540-
<Text style={[typography.h3, { color: colors.text_primary }]}>{watchedDescription}</Text>
541-
</View>
542-
<View style={styles.reviewItem}>
543-
<Text style={[typography.caption, { color: colors.muted }]}>CATEGORY</Text>
544-
<Text style={[typography.bodyLarge, { color: colors.text_primary, fontWeight: '700' }]}>
545-
{category ? categoryLabels[category] : 'Uncategorized'}
546-
</Text>
547-
</View>
548-
<View style={styles.reviewItem}>
549-
<Text style={[typography.caption, { color: colors.muted }]}>RECURRING</Text>
550-
<Text style={[typography.bodyLarge, { color: colors.text_primary, fontWeight: '700' }]}>
551-
{recurringEnabled ? recurringFrequencyLabels[recurringFrequency] : 'One-time'}
552-
</Text>
553-
</View>
554-
<View style={styles.reviewItem}>
555-
<Text style={[typography.caption, { color: colors.muted }]}>TOTAL AMOUNT</Text>
556-
<Text style={[typography.h1, { color: colors.primary }]}>
557-
{formatCurrencyFromCents(totalAmount, group?.currency ?? 'USD')}
558-
</Text>
559-
</View>
560-
<View style={styles.reviewItem}>
561-
<Text style={[typography.caption, { color: colors.muted }]}>PAID BY</Text>
562-
<View style={styles.payerRow}>
563-
<Avatar name={payerMember?.name ?? 'U'} size={24} />
539+
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: spacing.xl }}>
540+
<Card variant="elevated" style={styles.reviewCard}>
541+
<View style={styles.reviewItem}>
542+
<Text style={[typography.caption, { color: colors.muted }]}>DESCRIPTION</Text>
543+
<Text style={[typography.h3, { color: colors.text_primary }]}>{watchedDescription}</Text>
544+
</View>
545+
<View style={styles.reviewItem}>
546+
<Text style={[typography.caption, { color: colors.muted }]}>CATEGORY</Text>
564547
<Text style={[typography.bodyLarge, { color: colors.text_primary, fontWeight: '700' }]}>
565-
{payerMember?.name ?? 'Unknown'}
548+
{category ? categoryLabels[category] : 'Uncategorized'}
566549
</Text>
567550
</View>
568-
</View>
551+
<View style={styles.reviewItem}>
552+
<Text style={[typography.caption, { color: colors.muted }]}>RECURRING</Text>
553+
<Text style={[typography.bodyLarge, { color: colors.text_primary, fontWeight: '700' }]}>
554+
{recurringEnabled ? recurringFrequencyLabels[recurringFrequency] : 'One-time'}
555+
</Text>
556+
</View>
557+
<View style={styles.reviewItem}>
558+
<Text style={[typography.caption, { color: colors.muted }]}>TOTAL AMOUNT</Text>
559+
<Text style={[typography.h1, { color: colors.primary }]}>
560+
{formatCurrencyFromCents(totalAmount, group?.currency ?? 'USD')}
561+
</Text>
562+
</View>
563+
<View style={styles.reviewItem}>
564+
<Text style={[typography.caption, { color: colors.muted }]}>PAID BY</Text>
565+
<View style={styles.payerRow}>
566+
<Avatar name={payerMember?.name ?? 'U'} size={24} />
567+
<Text style={[typography.bodyLarge, { color: colors.text_primary, fontWeight: '700' }]}>
568+
{payerMember?.name ?? 'Unknown'}
569+
</Text>
570+
</View>
571+
</View>
572+
573+
<View style={[styles.divider, { backgroundColor: colors.border }]} />
569574

570-
<View style={[styles.divider, { backgroundColor: colors.border }]} />
571-
572-
<Text style={[typography.caption, { color: colors.muted, marginBottom: spacing.md }]}>
573-
SPLITS ({splitType.toUpperCase()})
574-
</Text>
575-
{selectedParticipantIds.map((userId) => {
576-
const member = members.find((m) => m.userId === userId);
577-
const shareAmount = shares[userId] ?? 0;
578-
return (
579-
<View key={userId} style={styles.splitRow}>
580-
<View style={styles.splitUserCol}>
581-
<Avatar name={member?.name ?? 'U'} size={20} />
582-
<Text style={[typography.bodyMedium, { color: colors.text_primary, fontWeight: '600' }]}>
583-
{member?.name ?? 'Unknown'}
575+
<Text style={[typography.caption, { color: colors.muted, marginBottom: spacing.md }]}>
576+
SPLITS ({splitType.toUpperCase()})
577+
</Text>
578+
{selectedParticipantIds.map((userId) => {
579+
const member = members.find((m) => m.userId === userId);
580+
const shareAmount = shares[userId] ?? 0;
581+
return (
582+
<View key={userId} style={styles.splitRow}>
583+
<View style={styles.splitUserCol}>
584+
<Avatar name={member?.name ?? 'U'} size={20} />
585+
<Text style={[typography.bodyMedium, { color: colors.text_primary, fontWeight: '600' }]}>
586+
{member?.name ?? 'Unknown'}
587+
</Text>
588+
</View>
589+
<Text style={[typography.bodyMedium, { color: colors.text_primary, fontWeight: '800' }]}>
590+
{formatCurrencyFromCents(shareAmount, group?.currency ?? 'USD')}
584591
</Text>
585592
</View>
586-
<Text style={[typography.bodyMedium, { color: colors.text_primary, fontWeight: '800' }]}>
587-
{formatCurrencyFromCents(shareAmount, group?.currency ?? 'USD')}
588-
</Text>
589-
</View>
590-
);
591-
})}
592-
</Card>
593+
);
594+
})}
595+
</Card>
596+
</ScrollView>
593597
</Animated.View>
594598
);
595599
}

apps/mobile/app/screens/GroupDetailScreen.tsx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ export function GroupDetailScreen({
359359
try {
360360
setExporting(true);
361361
const csv = await expenseService.exportCsv(route.params.groupId);
362-
const fileName = `fairshare-${route.params.groupId}.csv`;
362+
const fileName = `fairshare-expenses-${route.params.groupId}.csv`;
363363
const targetPath = `${FileSystem.cacheDirectory ?? FileSystem.documentDirectory}${fileName}`;
364364
await FileSystem.writeAsStringAsync(targetPath, csv, {
365365
encoding: FileSystem.EncodingType.UTF8,
@@ -386,6 +386,37 @@ export function GroupDetailScreen({
386386
}
387387
};
388388

389+
const handleExportBalancesCsv = async () => {
390+
try {
391+
setExporting(true);
392+
const csv = await groupService.exportBalancesCsv(route.params.groupId);
393+
const fileName = `fairshare-balances-${route.params.groupId}.csv`;
394+
const targetPath = `${FileSystem.cacheDirectory ?? FileSystem.documentDirectory}${fileName}`;
395+
await FileSystem.writeAsStringAsync(targetPath, csv, {
396+
encoding: FileSystem.EncodingType.UTF8,
397+
});
398+
399+
if (await Sharing.isAvailableAsync()) {
400+
await Sharing.shareAsync(targetPath, {
401+
mimeType: 'text/csv',
402+
dialogTitle: 'Export balances CSV',
403+
UTI: 'public.comma-separated-values-text',
404+
});
405+
} else {
406+
await Share.share({
407+
title: 'FairShare Balances CSV Export',
408+
message: csv,
409+
});
410+
}
411+
412+
toast('Balances CSV exported');
413+
} catch {
414+
toast('Failed to export balances CSV');
415+
} finally {
416+
setExporting(false);
417+
}
418+
};
419+
389420
if (loading) return <SkeletonList rows={5} />;
390421

391422
const renderExpenseSection = (title: string, expenseGroup: ExpenseDto[], delay: number) => {
@@ -551,7 +582,8 @@ export function GroupDetailScreen({
551582
<View style={styles.actionRow}>
552583
<Button variant="primary" onPress={() => navigation.navigate('AddExpense', { groupId: route.params.groupId })} style={styles.actionButton}>Add Expense</Button>
553584
<Button variant="secondary" onPress={() => navigation.navigate('SettleUp', { groupId: route.params.groupId })} style={styles.actionButton}>Settle Up</Button>
554-
<Button variant="secondary" onPress={() => void handleExportCsv()} loading={exporting} style={styles.actionButton}>Export CSV</Button>
585+
<Button variant="secondary" onPress={() => void handleExportCsv()} loading={exporting} style={styles.actionButton}>Export Expenses</Button>
586+
<Button variant="secondary" onPress={() => void handleExportBalancesCsv()} loading={exporting} style={styles.actionButton}>Export Balances</Button>
555587
</View>
556588

557589
{renderExpenseSection('Today', groupedExpenses.today, 300)}

apps/mobile/app/screens/HomeScreen.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,6 @@ export function HomeScreen({ navigation }: { navigation: any }) {
145145
nextAttentionItems
146146
.filter((item) => item.settlementCount > 0 || item.dueRecurringCount > 0)
147147
.sort((a, b) => (b.settlementCount + b.dueRecurringCount) - (a.settlementCount + a.dueRecurringCount))
148-
.slice(0, 4),
149148
);
150149
} catch (err) {
151150
console.error(err);
@@ -229,7 +228,7 @@ export function HomeScreen({ navigation }: { navigation: any }) {
229228
<>
230229
<SectionHeader title="Needs Attention" />
231230
<View style={styles.attentionList}>
232-
{attentionItems.map((item, index) => (
231+
{attentionItems.slice(0, 3).map((item, index) => (
233232
<Animated.View key={item.groupId} entering={FadeInDown.duration(400).delay(250 + index * 80)}>
234233
<TouchableOpacity
235234
activeOpacity={0.85}
@@ -269,6 +268,17 @@ export function HomeScreen({ navigation }: { navigation: any }) {
269268
</TouchableOpacity>
270269
</Animated.View>
271270
))}
271+
{attentionItems.length > 3 && (
272+
<Animated.View entering={FadeInDown.duration(400).delay(250 + 3 * 80)}>
273+
<TouchableOpacity
274+
activeOpacity={0.8}
275+
style={[styles.attentionCard, { backgroundColor: colors.surface, borderColor: colors.border, alignItems: 'center', justifyContent: 'center', paddingVertical: spacing.lg }]}
276+
onPress={() => navigation.navigate('Groups')}
277+
>
278+
<Text style={{ color: colors.primary, fontWeight: '800', fontSize: 14 }}>View all {attentionItems.length} items in Groups</Text>
279+
</TouchableOpacity>
280+
</Animated.View>
281+
)}
272282
</View>
273283
</>
274284
) : null}

apps/mobile/app/services/group.service.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export const groupService = {
3535
remindSettlement: async (id: string, payload: RemindSettlementRequestDto) =>
3636
(await api.post<RemindSettlementResponseDto>(`/groups/${id}/remind-settlement`, payload)).data,
3737
balances: async (id: string) => (await api.get(`/groups/${id}/balances`)).data,
38+
exportBalancesCsv: async (id: string) =>
39+
(await api.get<string>(`/groups/${id}/balances/export.csv`)).data,
3840
userSummary: async () => (await api.get<{ totalBalanceCents: string }>('/groups/summary')).data,
3941
simplify: async (id: string) => (await api.get(`/groups/${id}/simplify`)).data,
4042
activity: async (id: string, cursor = 0, limit = 20) =>

apps/web/app/layout.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Providers } from '../src/components/Providers';
66
import { themeStylesheet } from '../src/design/theme';
77
import { Navbar } from '../components/layout/Navbar';
88
import { Footer } from '../components/layout/Footer';
9+
import { CookieConsent } from '../src/components/ui/CookieConsent';
910

1011
export const metadata: Metadata = {
1112
title: 'FairShare - Smart Expense Sharing',
@@ -17,11 +18,20 @@ export const metadata: Metadata = {
1718
url: 'https://fairshare.app',
1819
siteName: 'FairShare',
1920
type: 'website',
21+
images: [
22+
{
23+
url: 'https://fairshare.app/og-image.png',
24+
width: 1200,
25+
height: 630,
26+
alt: 'FairShare - Smart Expense Sharing',
27+
},
28+
],
2029
},
2130
twitter: {
2231
card: 'summary_large_image',
2332
title: 'FairShare - Smart Expense Sharing',
2433
description: 'Split group expenses without confusion. FairShare helps groups track spending and settle balances faster.',
34+
images: ['https://fairshare.app/og-image.png'],
2535
},
2636
icons: {
2737
icon: '/favicon.ico',
@@ -42,6 +52,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
4252
<Navbar />
4353
<div>{children}</div>
4454
<Footer />
55+
<CookieConsent />
4556
</Providers>
4657
</body>
4758
</html>

0 commit comments

Comments
 (0)