Update CBS Mobile App codebase
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import React, { memo, useState } from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import Ionicons from '@react-native-vector-icons/ionicons';
|
||||
|
||||
import type { CbsAccount } from '../../types/cbs';
|
||||
import { useTheme, textStyles } from '../../theme';
|
||||
|
||||
interface AccountCardProps {
|
||||
account: CbsAccount;
|
||||
/**
|
||||
* The customer's name, from the session. Passed in rather than read here:
|
||||
* this is a shared presentational primitive and none of them reach into app
|
||||
* state. It is also not account data — `/accounts` returns no holder name.
|
||||
*/
|
||||
accountHolder?: string;
|
||||
/**
|
||||
* Whether the reveal control for the account number is offered. When false
|
||||
* the number stays masked with no way to reveal it.
|
||||
*/
|
||||
canRevealNumber?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The hero card for one account.
|
||||
*
|
||||
* This was a balance card until the app moved to `GET /accounts`, which
|
||||
* returns no balance — and no other CBS endpoint returns one either. Rather
|
||||
* than print a figure the bank never sent, the card now leads with what it can
|
||||
* actually vouch for: which account this is, and whether it is usable.
|
||||
*
|
||||
* The account number is masked by default. `/accounts` is the only endpoint
|
||||
* that returns an unmasked one, so revealing it is a deliberate act.
|
||||
*/
|
||||
const AccountCardComponent = ({
|
||||
account,
|
||||
accountHolder,
|
||||
canRevealNumber = true,
|
||||
}: AccountCardProps) => {
|
||||
const theme = useTheme();
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
|
||||
const showFull = revealed && canRevealNumber;
|
||||
const displayNumber = showFull
|
||||
? account.accountNumber
|
||||
: `XXXX ${account.accountNumber.slice(-4)}`;
|
||||
|
||||
return (
|
||||
<LinearGradient
|
||||
colors={theme.colors.brandGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={[styles.card, styles.gradientCard, { borderRadius: theme.radius.xl }]}
|
||||
>
|
||||
<View style={[styles.brandAccent, { backgroundColor: theme.colors.onBrandSurface }]} />
|
||||
|
||||
<View style={styles.topRow}>
|
||||
<View style={styles.bankInfo}>
|
||||
<View
|
||||
style={[styles.bankIconFrame, { backgroundColor: theme.colors.onBrandSurfaceStrong }]}
|
||||
>
|
||||
<Ionicons name="business" size={20} color={theme.colors.onBrandPrimary} />
|
||||
</View>
|
||||
<View>
|
||||
<Text style={[styles.bankName, { color: theme.colors.onBrandPrimary }]}>
|
||||
CBS Bank
|
||||
</Text>
|
||||
<Text style={[styles.cardType, { color: theme.colors.onBrandSecondary }]}>
|
||||
{`${account.accountType.toUpperCase()} ACCOUNT`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* A non-active account has to say so here: this is the card the
|
||||
customer picks an account from, and dormant is the reason a later
|
||||
transfer would be refused. */}
|
||||
{account.status !== 'ACTIVE' ? (
|
||||
<View style={[styles.statusBadge, { backgroundColor: theme.colors.onBrandPrimary }]}>
|
||||
<Text style={[styles.statusBadgeText, { color: theme.colors.primary }]}>
|
||||
{account.status}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<View style={styles.mainContent}>
|
||||
<Text style={[styles.numberLabel, { color: theme.colors.onBrandSecondary }]}>
|
||||
ACCOUNT NUMBER
|
||||
</Text>
|
||||
<View style={styles.numberRow}>
|
||||
<Text
|
||||
style={[styles.numberValue, { color: theme.colors.onBrandPrimary }]}
|
||||
selectable={showFull}
|
||||
>
|
||||
{displayNumber}
|
||||
</Text>
|
||||
{canRevealNumber ? (
|
||||
<Pressable
|
||||
onPress={() => setRevealed((current) => !current)}
|
||||
style={[styles.revealButton, { backgroundColor: theme.colors.onBrandSurface }]}
|
||||
hitSlop={8}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={showFull ? 'Hide account number' : 'Show account number'}
|
||||
>
|
||||
<Ionicons
|
||||
name={showFull ? 'eye-off-outline' : 'eye-outline'}
|
||||
size={20}
|
||||
color={theme.colors.onBrandPrimary}
|
||||
/>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.footerRow}>
|
||||
<View style={styles.footerItem}>
|
||||
<Text style={[styles.footerLabel, { color: theme.colors.onBrandMuted }]}>
|
||||
{accountHolder ? 'ACCOUNT HOLDER' : 'IFSC'}
|
||||
</Text>
|
||||
<Text style={[styles.footerValue, { color: theme.colors.onBrandPrimary }]}>
|
||||
{accountHolder ?? account.ifsc}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[styles.footerItem, styles.footerItemEnd]}>
|
||||
<Text style={[styles.footerLabel, { color: theme.colors.onBrandMuted }]}>
|
||||
{account.mmid ? 'MMID' : 'IFSC'}
|
||||
</Text>
|
||||
<Text style={[styles.footerValue, { color: theme.colors.onBrandPrimary }]}>
|
||||
{account.mmid ?? account.ifsc}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
padding: 24,
|
||||
gap: 32,
|
||||
position: 'relative',
|
||||
minHeight: 260,
|
||||
},
|
||||
gradientCard: {
|
||||
overflow: 'hidden',
|
||||
},
|
||||
brandAccent: {
|
||||
position: 'absolute',
|
||||
top: -50,
|
||||
right: -50,
|
||||
width: 200,
|
||||
height: 200,
|
||||
borderRadius: 100,
|
||||
},
|
||||
topRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
bankInfo: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
bankIconFrame: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
bankName: {
|
||||
...textStyles.h3,
|
||||
},
|
||||
cardType: {
|
||||
...textStyles.eyebrow,
|
||||
fontSize: 10,
|
||||
},
|
||||
statusBadge: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 10,
|
||||
},
|
||||
statusBadgeText: {
|
||||
...textStyles.eyebrow,
|
||||
fontSize: 10,
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
mainContent: {
|
||||
gap: 6,
|
||||
},
|
||||
numberLabel: {
|
||||
...textStyles.eyebrow,
|
||||
letterSpacing: 1,
|
||||
},
|
||||
numberRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
// An account number, so it is set in mono — a customer reads these back a
|
||||
// group at a time.
|
||||
numberValue: {
|
||||
...textStyles.code,
|
||||
flex: 1,
|
||||
fontSize: 22,
|
||||
letterSpacing: 1.5,
|
||||
},
|
||||
revealButton: {
|
||||
marginLeft: 12,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
footerRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-end',
|
||||
marginTop: 4,
|
||||
},
|
||||
footerItem: {
|
||||
gap: 4,
|
||||
flexShrink: 1,
|
||||
},
|
||||
footerItemEnd: {
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
footerLabel: {
|
||||
...textStyles.eyebrow,
|
||||
fontSize: 9,
|
||||
letterSpacing: 1,
|
||||
},
|
||||
footerValue: {
|
||||
...textStyles.bodyStrong,
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
|
||||
export const AccountCard = memo(AccountCardComponent);
|
||||
Reference in New Issue
Block a user