Files
CBS-Mobile-App/src/components/TransactionItem/index.tsx
T
Abin ToppoandClaude Opus 5.5 f960a119e8 Add Positive Pay, statement guide v1.0, onboarding, and switch every feature on
- Positive Pay on the 12 Oct /pps contract (account number, not account ID):
  OTP enrollment, cheque submit, saved cheques, detail and stop payment.
  Full flow passed on the phone against mobile-banking-backend.lincpay.
- Statement & Mini Statement guide v1.0 (14 Oct): mini statement JSON on
  Home and the Statement screen; dated PDF export with the password rule.
- Onboarding slides and splash screen, Inter font, shared Icon and
  TypewriterText components, nearby branches via OpenStreetMap.
- Every feature switched on; the 14 Oct switch set is kept in
  featureConfig.stage-2026-10-14.ts. Loans run on demo data until the
  backend has /loans routes.
- npm run android wraps run-android for shells spawned from VS Code.
- OpenProject work packages updated to 14-10-2026 status, with screenshots
  of Positive Pay, statement, loans and cards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:18:40 +05:30

190 lines
5.0 KiB
TypeScript

import React, { memo, useMemo } from 'react';
import {
StyleSheet,
Text,
View,
} from 'react-native';
import { Icon } from '../Icon';
import type { TransactionRecord } from '../../types/app';
import { useTheme, textStyles } from '../../theme';
import { formatCurrency, formatDate } from '../../utils/formatters';
interface TransactionItemProps {
item: TransactionRecord;
}
type RiskLevel = 'safe' | 'medium' | 'high';
const TransactionItemComponent = ({ item }: TransactionItemProps) => {
const theme = useTheme();
const isCredit = item.direction === 'credit';
const riskLevel = useMemo<RiskLevel>(() => {
const sum = item.id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
if (sum % 10 < 7) return 'safe';
if (sum % 10 < 9) return 'medium';
return 'high';
}, [item.id]);
const riskConfig = {
safe: {
color: theme.colors.success,
surface: theme.colors.successSurface,
border: theme.colors.successBorder,
label: 'SECURE',
icon: 'shield-checkmark' as const,
},
medium: {
color: theme.colors.warning,
surface: theme.colors.warningSurface,
border: theme.colors.warningBorder,
label: 'FLAGGED',
icon: 'warning' as const,
},
high: {
color: theme.colors.danger,
surface: theme.colors.dangerSurface,
border: theme.colors.dangerBorder,
label: 'High risk',
icon: 'alert-circle' as const,
},
}[riskLevel];
return (
<View style={[styles.wrapper, { borderBottomColor: theme.colors.cardBorder }]}>
<View style={styles.leading}>
<View
style={[
styles.icon,
{
backgroundColor: isCredit
? theme.colors.successSurface
: theme.colors.surfaceMuted,
borderColor: isCredit ? theme.colors.successBorder : theme.colors.cardBorder,
},
]}
>
<Icon
name={isCredit ? 'chevron-down' : 'chevron-up'}
size={18}
color={isCredit ? theme.colors.success : theme.colors.textSecondary}
/>
</View>
<View style={styles.copy}>
<View style={styles.titleRow}>
<Text style={[styles.title, { color: theme.colors.textPrimary }]}>{item.title}</Text>
</View>
<View style={styles.subtitleRow}>
<Text style={[styles.subtitle, { color: theme.colors.textMuted }]}>
{item.counterparty}
</Text>
<View style={[styles.dot, { backgroundColor: theme.colors.textMuted }]} />
<Text style={[styles.subtitle, { color: theme.colors.textMuted }]}>
{formatDate(item.occurredAt)}
</Text>
</View>
</View>
</View>
<View style={styles.trailing}>
<Text
style={[
styles.amount,
{ color: isCredit ? theme.colors.success : theme.colors.textPrimary },
]}
>
{isCredit ? '+' : '-'}
{formatCurrency(item.amount)}
</Text>
<View
style={[
styles.riskBadge,
{ backgroundColor: riskConfig.surface, borderColor: riskConfig.border },
]}
>
<Text style={[styles.riskText, { color: riskConfig.color }]}>{riskConfig.label}</Text>
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
wrapper: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 18,
borderBottomWidth: 1,
},
leading: {
flexDirection: 'row',
alignItems: 'center',
gap: 16,
flex: 1,
},
icon: {
width: 46,
height: 46,
borderRadius: 23,
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
},
copy: {
gap: 4,
flex: 1,
},
titleRow: {
flexDirection: 'row',
alignItems: 'center',
},
title: {
...textStyles.h3,
},
subtitleRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
dot: {
width: 3,
height: 3,
borderRadius: 1.5,
backgroundColor: 'transparent', // set from the theme at the call site
},
subtitle: {
...textStyles.caption,
},
trailing: {
alignItems: 'flex-end',
gap: 4,
},
amount: {
...textStyles.money,
},
riskBadge: {
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 6,
borderWidth: 1,
},
// Takes its family and casing from `eyebrow`, then overrides the size.
//
// 8pt is below the scale's floor of 11 and the scale has no entry for it on
// purpose — at this size the label is decoration, not something a customer
// can be expected to read. Left at 8 rather than snapped up to 11 because
// the badge is laid out around it; worth revisiting as a design question,
// since a risk flag nobody can read is not doing its job.
riskText: {
...textStyles.eyebrow,
fontSize: 8,
lineHeight: 11,
letterSpacing: 0.5,
},
});
export const TransactionItem = memo(TransactionItemComponent);