- 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>
302 lines
7.8 KiB
TypeScript
302 lines
7.8 KiB
TypeScript
import React, { memo } from 'react';
|
|
import {
|
|
Pressable,
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
} from 'react-native';
|
|
import { Icon } from '../../../components/Icon';
|
|
import LinearGradient from 'react-native-linear-gradient';
|
|
import Animated, {
|
|
Extrapolation,
|
|
interpolate,
|
|
SharedValue,
|
|
useAnimatedStyle,
|
|
useSharedValue,
|
|
withTiming,
|
|
} from 'react-native-reanimated';
|
|
|
|
import type { BankCard } from '../../../types/app';
|
|
import { getCardStatusLabel } from '../utils/cardCopy';
|
|
import { fontFamily } from '../../../theme';
|
|
|
|
interface CardItemProps {
|
|
card: BankCard;
|
|
index: number;
|
|
scrollX: SharedValue<number>;
|
|
cardWidth: number;
|
|
itemSize: number;
|
|
height?: number;
|
|
}
|
|
|
|
const CardItemComponent = ({
|
|
card,
|
|
index,
|
|
scrollX,
|
|
cardWidth,
|
|
itemSize,
|
|
height = 220,
|
|
}: CardItemProps) => {
|
|
const flipProgress = useSharedValue(0);
|
|
const statusLabel = getCardStatusLabel(card);
|
|
const statusColor =
|
|
card.status === 'blocked'
|
|
? '#FFB3BC'
|
|
: card.controls.frozen
|
|
? '#FFE08C'
|
|
: '#C8FACC';
|
|
|
|
const toggleFlip = () => {
|
|
flipProgress.value = withTiming(flipProgress.value === 0 ? 1 : 0, { duration: 420 });
|
|
};
|
|
|
|
const carouselStyle = useAnimatedStyle(() => {
|
|
const inputRange = [(index - 1) * itemSize, index * itemSize, (index + 1) * itemSize];
|
|
|
|
return {
|
|
transform: [
|
|
{
|
|
scale: interpolate(scrollX.value, inputRange, [0.92, 1, 0.92], Extrapolation.CLAMP),
|
|
},
|
|
{
|
|
translateY: interpolate(scrollX.value, inputRange, [12, 0, 12], Extrapolation.CLAMP),
|
|
},
|
|
],
|
|
opacity: interpolate(scrollX.value, inputRange, [0.72, 1, 0.72], Extrapolation.CLAMP),
|
|
};
|
|
});
|
|
|
|
const frontStyle = useAnimatedStyle(() => ({
|
|
transform: [
|
|
{ perspective: 1200 },
|
|
{ rotateY: `${interpolate(flipProgress.value, [0, 1], [0, 180])}deg` },
|
|
],
|
|
}));
|
|
|
|
const backStyle = useAnimatedStyle(() => ({
|
|
transform: [
|
|
{ perspective: 1200 },
|
|
{ rotateY: `${interpolate(flipProgress.value, [0, 1], [180, 360])}deg` },
|
|
],
|
|
}));
|
|
|
|
return (
|
|
<Animated.View style={[styles.frame, carouselStyle, { width: cardWidth, height }]}>
|
|
<Pressable style={styles.flex} onPress={toggleFlip}>
|
|
<Animated.View style={[styles.face, frontStyle]}>
|
|
<LinearGradient colors={card.themeColors} style={styles.gradient}>
|
|
<View style={styles.headerRow}>
|
|
<View>
|
|
<Text style={styles.brandName}>CBS Bank</Text>
|
|
<Text style={styles.cardMeta}>{card.type.toUpperCase()} CARD</Text>
|
|
</View>
|
|
<View style={styles.statusBadge}>
|
|
<View style={[styles.statusDot, { backgroundColor: statusColor }]} />
|
|
<Text style={styles.statusBadgeText}>{statusLabel}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.centerBlock}>
|
|
<Text style={styles.cardName}>{card.name}</Text>
|
|
<Text style={styles.cardNumber}>{card.maskedNumber}</Text>
|
|
<Text style={styles.holderName}>{card.cardholderName}</Text>
|
|
</View>
|
|
|
|
<View style={styles.footerRow}>
|
|
<View>
|
|
<Text style={styles.footerLabel}>VALID THRU</Text>
|
|
<Text style={styles.footerValue}>{card.expiry}</Text>
|
|
</View>
|
|
<View>
|
|
<Text style={styles.footerLabel}>NETWORK</Text>
|
|
<Text style={styles.footerValue}>{card.network}</Text>
|
|
</View>
|
|
<View style={styles.flipHint}>
|
|
<Icon name="sync-outline" size={14} color="rgba(255,255,255,0.85)" />
|
|
<Text style={styles.flipText}>Flip</Text>
|
|
</View>
|
|
</View>
|
|
</LinearGradient>
|
|
</Animated.View>
|
|
|
|
<Animated.View style={[styles.face, styles.backFace, backStyle]}>
|
|
<LinearGradient
|
|
colors={[card.themeColors[2], card.themeColors[1], card.themeColors[0]]}
|
|
style={styles.gradient}
|
|
>
|
|
<View style={styles.magneticStrip} />
|
|
<View style={styles.backSection}>
|
|
<Text style={styles.backLabel}>PRIMARY LINKED ACCOUNT</Text>
|
|
<Text style={styles.backValue}>{card.primaryLinkedAccountId}</Text>
|
|
</View>
|
|
<View style={styles.backGrid}>
|
|
<View style={styles.backCell}>
|
|
<Text style={styles.backLabel}>DAILY LIMIT</Text>
|
|
<Text style={styles.backValue}>Rs {card.limits.daily.toLocaleString('en-IN')}</Text>
|
|
</View>
|
|
<View style={styles.backCell}>
|
|
<Text style={styles.backLabel}>LINKED ACCOUNTS</Text>
|
|
<Text style={styles.backValue}>{card.linkedAccountIds.length}</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.backFooter}>
|
|
<Text style={styles.backSupport}>24x7 card care · 1800 100 2244</Text>
|
|
<Text style={styles.backSupport}>Tap card again to return</Text>
|
|
</View>
|
|
</LinearGradient>
|
|
</Animated.View>
|
|
</Pressable>
|
|
</Animated.View>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
flex: {
|
|
flex: 1,
|
|
},
|
|
frame: {
|
|
overflow: 'visible',
|
|
},
|
|
face: {
|
|
...StyleSheet.absoluteFillObject,
|
|
backfaceVisibility: 'hidden',
|
|
},
|
|
backFace: {
|
|
zIndex: 2,
|
|
},
|
|
gradient: {
|
|
flex: 1,
|
|
borderRadius: 28,
|
|
padding: 22,
|
|
justifyContent: 'space-between',
|
|
overflow: 'hidden',
|
|
},
|
|
headerRow: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'flex-start',
|
|
gap: 12,
|
|
},
|
|
brandName: {
|
|
color: 'white',
|
|
fontSize: 18,
|
|
fontFamily: fontFamily.bold,
|
|
letterSpacing: -0.3,
|
|
},
|
|
cardMeta: {
|
|
color: 'rgba(255,255,255,0.72)',
|
|
fontSize: 10,
|
|
fontFamily: fontFamily.bold,
|
|
letterSpacing: 1.1,
|
|
marginTop: 2,
|
|
},
|
|
statusBadge: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
borderRadius: 999,
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 8,
|
|
backgroundColor: 'rgba(255,255,255,0.14)',
|
|
},
|
|
statusDot: {
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: 4,
|
|
},
|
|
statusBadgeText: {
|
|
color: 'white',
|
|
fontSize: 11,
|
|
fontFamily: fontFamily.bold,
|
|
},
|
|
centerBlock: {
|
|
gap: 10,
|
|
},
|
|
cardName: {
|
|
color: 'rgba(255,255,255,0.82)',
|
|
fontSize: 12,
|
|
fontFamily: fontFamily.bold,
|
|
letterSpacing: 0.1,
|
|
},
|
|
cardNumber: {
|
|
color: 'white',
|
|
fontSize: 28,
|
|
fontFamily: fontFamily.bold,
|
|
letterSpacing: 2,
|
|
},
|
|
holderName: {
|
|
color: 'rgba(255,255,255,0.92)',
|
|
fontSize: 14,
|
|
fontFamily: fontFamily.bold,
|
|
letterSpacing: 0.6,
|
|
},
|
|
footerRow: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'flex-end',
|
|
gap: 12,
|
|
},
|
|
footerLabel: {
|
|
color: 'rgba(255,255,255,0.68)',
|
|
fontSize: 9,
|
|
fontFamily: fontFamily.bold,
|
|
letterSpacing: 1,
|
|
},
|
|
footerValue: {
|
|
color: 'white',
|
|
fontSize: 13,
|
|
fontFamily: fontFamily.bold,
|
|
marginTop: 4,
|
|
},
|
|
flipHint: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 6,
|
|
},
|
|
flipText: {
|
|
color: 'rgba(255,255,255,0.9)',
|
|
fontSize: 12,
|
|
fontFamily: fontFamily.bold,
|
|
},
|
|
magneticStrip: {
|
|
height: 42,
|
|
marginHorizontal: -22,
|
|
backgroundColor: 'rgba(7, 19, 33, 0.8)',
|
|
},
|
|
backSection: {
|
|
gap: 6,
|
|
},
|
|
backGrid: {
|
|
flexDirection: 'row',
|
|
gap: 12,
|
|
},
|
|
backCell: {
|
|
flex: 1,
|
|
gap: 6,
|
|
padding: 14,
|
|
borderRadius: 16,
|
|
backgroundColor: 'rgba(255,255,255,0.12)',
|
|
},
|
|
backLabel: {
|
|
color: 'rgba(255,255,255,0.68)',
|
|
fontSize: 10,
|
|
fontFamily: fontFamily.bold,
|
|
letterSpacing: 0.9,
|
|
},
|
|
backValue: {
|
|
color: 'white',
|
|
fontSize: 14,
|
|
fontFamily: fontFamily.bold,
|
|
},
|
|
backFooter: {
|
|
gap: 6,
|
|
},
|
|
backSupport: {
|
|
color: 'rgba(255,255,255,0.82)',
|
|
fontSize: 12,
|
|
fontFamily: fontFamily.semibold,
|
|
},
|
|
});
|
|
|
|
export const CardItem = memo(CardItemComponent);
|