Files
CBS-Mobile-App/src/modules/home/screens/ReKycScreen.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

786 lines
24 KiB
TypeScript

import React, { useState } from 'react';
import {
Alert,
Pressable,
StyleSheet,
Text,
View,
} from 'react-native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { Icon } from '../../../components/Icon';
import LinearGradient from 'react-native-linear-gradient';
import { BottomSheet, Button, Card, Input, ScreenShell } from '../../../components';
import { useAccounts } from '../hooks/useAccounts';
import type { HomeStackParamList } from '../../../navigation/types';
import { useTheme, fontFamily } from '../../../theme';
type Props = NativeStackScreenProps<HomeStackParamList, 'ReKyc'>;
type IconName = React.ComponentProps<typeof Icon>['name'];
type ChangeField = 'name' | 'mobile' | 'email' | 'address';
type ChangeOption = {
id: ChangeField;
label: string;
icon: IconName;
};
const CHANGE_OPTIONS: ChangeOption[] = [
{ id: 'name', label: 'Name', icon: 'person-outline' },
{ id: 'mobile', label: 'Mobile', icon: 'call-outline' },
{ id: 'email', label: 'Email', icon: 'mail-outline' },
{ id: 'address', label: 'Address', icon: 'location-outline' },
];
const STEPS = [
'Enter your Aadhaar number',
'Upload documents & latest photograph',
'Accept the self-declaration and submit',
];
type DocOption = { id: string; label: string; hint: string; icon: IconName };
const DOCUMENT_OPTIONS: DocOption[] = [
{ id: 'identity', label: 'Identity proof', hint: 'Aadhaar / Passport / Voter ID', icon: 'id-card-outline' },
{ id: 'address', label: 'Address proof', hint: 'Utility bill / Rent agreement', icon: 'home-outline' },
{ id: 'pan', label: 'PAN card', hint: 'Required for tax records', icon: 'card-outline' },
];
const DECLARATIONS = [
'The information and documents provided are true and correct.',
'I will inform the bank of any future change in my KYC details.',
'I consent to the bank verifying my documents with the issuing authority.',
];
const formatAadhaar = (value: string) =>
value
.replace(/\D/g, '')
.slice(0, 12)
.replace(/(\d{4})(?=\d)/g, '$1 ');
const extractDigits = (value: string) => value.replace(/\D/g, '');
export default function ReKycScreen({ route, navigation }: Props) {
const theme = useTheme();
// Read from the bank on mount; nothing about accounts is cached locally.
// Re-KYC is raised against an operative account.
const { operableAccounts: accounts } = useAccounts();
const account =
accounts.find((item) => item.accountNumber === route.params?.accountNumber) ?? accounts[0];
const [aadhaarNumber, setAadhaarNumber] = useState('');
const [selectedFields, setSelectedFields] = useState<ChangeField[]>([]);
const [updatedName, setUpdatedName] = useState('');
const [updatedMobile, setUpdatedMobile] = useState('');
const [updatedEmail, setUpdatedEmail] = useState('');
const [updatedAddress, setUpdatedAddress] = useState('');
const [consentGiven, setConsentGiven] = useState(false);
const [uploadedDocs, setUploadedDocs] = useState<string[]>([]);
const [photoUploaded, setPhotoUploaded] = useState(false);
const [acceptedDeclarations, setAcceptedDeclarations] = useState<number[]>([]);
const [otpSheetVisible, setOtpSheetVisible] = useState(false);
const [otpValue, setOtpValue] = useState('');
const toggleDocument = (id: string) => {
setUploadedDocs((current) =>
current.includes(id) ? current.filter((item) => item !== id) : [...current, id],
);
};
const toggleDeclaration = (index: number) => {
setAcceptedDeclarations((current) =>
current.includes(index) ? current.filter((item) => item !== index) : [...current, index],
);
};
const allDeclarationsAccepted = acceptedDeclarations.length === DECLARATIONS.length;
if (!account) {
return (
<ScreenShell title=" Aadhaar" subtitle="Identity refresh">
<Card style={styles.emptyCard}>
<View
style={[
styles.emptyIcon,
{ backgroundColor: theme.tint(theme.colors.danger, 0.07) },
]}
>
<Icon name="alert-circle-outline" size={28} color={theme.colors.danger} />
</View>
<Text style={[styles.emptyTitle, { color: theme.colors.textPrimary }]}>
Account not found
</Text>
<Text style={[styles.emptyDescription, { color: theme.colors.textSecondary }]}>
We could not start re-KYC because the selected account is unavailable.
</Text>
</Card>
</ScreenShell>
);
}
const toggleField = (field: ChangeField) => {
setSelectedFields((currentValue) =>
currentValue.includes(field)
? currentValue.filter((item) => item !== field)
: [...currentValue, field],
);
};
const handleAadhaarChange = (value: string) => {
setAadhaarNumber(formatAadhaar(value));
};
const getMissingFieldLabel = () => {
if (selectedFields.includes('name') && updatedName.trim().length === 0) {
return 'updated name';
}
if (selectedFields.includes('mobile') && extractDigits(updatedMobile).length !== 10) {
return 'updated mobile number';
}
if (selectedFields.includes('email') && !updatedEmail.includes('@')) {
return 'updated email';
}
if (selectedFields.includes('address') && updatedAddress.trim().length < 8) {
return 'updated address';
}
return null;
};
const handleSubmit = () => {
const aadhaarDigits = extractDigits(aadhaarNumber);
if (aadhaarDigits.length !== 12) {
Alert.alert('Invalid Aadhaar', 'Enter a valid 12-digit Aadhaar number to continue.');
return;
}
if (selectedFields.length === 0 && !consentGiven) {
Alert.alert('Consent required', 'Please verify that there is no change on your Aadhaar details.');
return;
}
const missingFieldLabel = getMissingFieldLabel();
if (missingFieldLabel) {
Alert.alert('Incomplete details', `Please enter a valid ${missingFieldLabel}.`);
return;
}
if (!photoUploaded) {
Alert.alert('Photograph required', 'Please upload your latest photograph to continue.');
return;
}
if (!allDeclarationsAccepted) {
Alert.alert('Self declaration', 'Please accept all the self-declaration statements to continue.');
return;
}
setOtpSheetVisible(true);
};
const handleVerifyOtpAndSubmit = () => {
if (otpValue.length !== 6) {
Alert.alert('Invalid OTP', 'Please enter a valid 6-digit OTP.');
return;
}
setOtpSheetVisible(false);
setTimeout(() => {
Alert.alert(
'Re-KYC submitted',
'Your Aadhaar-based re-KYC request has been sent for review. We will update your records after validation.',
[
{
text: 'Back to account',
onPress: () => navigation.goBack(),
},
],
);
}, 300);
};
return (
<ScreenShell
title="Re-KYC via Aadhaar"
subtitle={`${account?.accountType ?? ""} account · A/c XXXX ${account?.accountNumber.slice(-4) ?? ""}`}
contentContainerStyle={styles.contentContainer}
>
<LinearGradient
colors={[theme.colors.surface, theme.colors.primarySurface, theme.colors.surface]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={[styles.heroCard, { borderColor: theme.colors.cardBorder }]}
>
<View style={styles.heroHeader}>
<View
style={[
styles.heroIcon,
{ backgroundColor: theme.tint(theme.colors.primary, 0.07) },
]}
>
<Icon name="shield-checkmark-outline" size={20} color={theme.colors.primary} />
</View>
<View style={styles.heroCopy}>
<Text style={[styles.heroTitle, { color: theme.colors.textPrimary }]}>
Update KYC when your records change
</Text>
<Text style={[styles.heroSubtitle, { color: theme.colors.textSecondary }]}>
Use Aadhaar re-verification if your name, mobile, email, or address has changed.
</Text>
</View>
</View>
<View style={styles.stepList}>
{STEPS.map((step, index) => (
<View key={step} style={styles.stepRow}>
<View
style={[
styles.stepIndex,
{ backgroundColor: theme.tint(theme.colors.primary, 0.07) },
]}
>
<Text style={[styles.stepIndexText, { color: theme.colors.primary }]}>
{index + 1}
</Text>
</View>
<Text style={[styles.stepText, { color: theme.colors.textPrimary }]}>{step}</Text>
</View>
))}
</View>
</LinearGradient>
<Card style={styles.sectionCard}>
<View style={styles.sectionHeader}>
<View
style={[
styles.sectionIcon,
{ backgroundColor: theme.tint(theme.colors.info, 0.07) },
]}
>
<Icon name="card-outline" size={18} color={theme.colors.info} />
</View>
<View style={styles.sectionCopy}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Aadhaar details
</Text>
<Text style={[styles.sectionSubtitle, { color: theme.colors.textSecondary }]}>
Enter the Aadhaar linked to your updated identity details
</Text>
</View>
</View>
<Input
label="Aadhaar number"
value={aadhaarNumber}
onChangeText={handleAadhaarChange}
keyboardType="number-pad"
maxLength={14}
placeholder="1234 5678 9012"
leftAccessory={<Icon name="finger-print-outline" size={18} color={theme.colors.textMuted} />}
/>
</Card>
<Card style={styles.sectionCard}>
<View style={styles.sectionHeader}>
<View
style={[
styles.sectionIcon,
{ backgroundColor: theme.tint(theme.colors.warning, 0.07) },
]}
>
<Icon name="create-outline" size={18} color={theme.colors.warning} />
</View>
<View style={styles.sectionCopy}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
What changed?
</Text>
<Text style={[styles.sectionSubtitle, { color: theme.colors.textSecondary }]}>
Choose the fields that need to be updated
</Text>
</View>
</View>
<View style={styles.chipGrid}>
{CHANGE_OPTIONS.map((option) => {
const isSelected = selectedFields.includes(option.id);
return (
<Pressable
key={option.id}
onPress={() => toggleField(option.id)}
style={[
styles.changeChip,
{
backgroundColor: isSelected ? theme.tint(theme.colors.primary, 0.07) : theme.colors.surfaceMuted,
borderColor: isSelected ? theme.tint(theme.colors.primary, 0.27) : theme.colors.cardBorder,
},
]}
>
<Icon
name={option.icon as any}
size={16}
color={isSelected ? theme.colors.primary : theme.colors.textSecondary}
/>
<Text
style={[
styles.changeChipText,
{ color: isSelected ? theme.colors.primary : theme.colors.textPrimary },
]}
>
{option.label}
</Text>
</Pressable>
);
})}
</View>
{selectedFields.length === 0 ? (
<Pressable
onPress={() => setConsentGiven((currentValue) => !currentValue)}
style={[
styles.consentCard,
{ backgroundColor: theme.colors.surfaceMuted, borderColor: theme.colors.cardBorder },
]}
>
<Icon
name={consentGiven ? 'checkmark-circle' : 'ellipse-outline'}
size={22}
color={consentGiven ? theme.colors.success : theme.colors.textMuted}
/>
<Text style={[styles.consentText, { color: theme.colors.textPrimary }]}>
I verify that there is no change on my Aadhaar details.
</Text>
</Pressable>
) : null}
{selectedFields.includes('name') ? (
<Input
label="Updated name"
value={updatedName}
onChangeText={setUpdatedName}
placeholder="Name as per Aadhaar"
autoCapitalize="words"
/>
) : null}
{selectedFields.includes('mobile') ? (
<Input
label="Updated mobile"
value={updatedMobile}
onChangeText={setUpdatedMobile}
keyboardType="phone-pad"
maxLength={10}
placeholder="10-digit mobile number"
/>
) : null}
{selectedFields.includes('email') ? (
<Input
label="Updated email"
value={updatedEmail}
onChangeText={setUpdatedEmail}
keyboardType="email-address"
autoCapitalize="none"
placeholder="name@example.com"
/>
) : null}
{selectedFields.includes('address') ? (
<Input
label="Updated address"
value={updatedAddress}
onChangeText={setUpdatedAddress}
placeholder="Flat, street, area, city"
/>
) : null}
</Card>
{/* Document upload */}
<Card style={styles.sectionCard}>
<View style={styles.sectionHeader}>
<View style={[styles.sectionIcon, { backgroundColor: theme.tint(theme.colors.success, 0.07) }]}>
<Icon name="cloud-upload-outline" size={18} color={theme.colors.success} />
</View>
<View style={styles.sectionCopy}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>Upload documents</Text>
<Text style={[styles.sectionSubtitle, { color: theme.colors.textSecondary }]}>
Attach the latest copies of your KYC documents
</Text>
</View>
</View>
{DOCUMENT_OPTIONS.map((doc) => {
const isUploaded = uploadedDocs.includes(doc.id);
return (
<Pressable
key={doc.id}
onPress={() => toggleDocument(doc.id)}
style={[
styles.uploadRow,
{
backgroundColor: isUploaded ? theme.tint(theme.colors.success, 0.06) : theme.colors.surfaceMuted,
borderColor: isUploaded ? theme.tint(theme.colors.success, 0.27) : theme.colors.cardBorder,
},
]}
>
<View style={[styles.uploadIcon, { backgroundColor: theme.colors.surface }]}>
<Icon name={doc.icon} size={18} color={theme.colors.primary} />
</View>
<View style={styles.uploadCopy}>
<Text style={[styles.uploadLabel, { color: theme.colors.textPrimary }]}>{doc.label}</Text>
<Text style={[styles.uploadHint, { color: theme.colors.textSecondary }]}>
{isUploaded ? 'Uploaded · tap to remove' : doc.hint}
</Text>
</View>
<Icon
name={isUploaded ? 'checkmark-circle' : 'add-circle-outline'}
size={22}
color={isUploaded ? theme.colors.success : theme.colors.primary}
/>
</Pressable>
);
})}
</Card>
{/* Latest photograph */}
<Card style={styles.sectionCard}>
<View style={styles.sectionHeader}>
<View style={[styles.sectionIcon, { backgroundColor: theme.tint(theme.colors.info, 0.07) }]}>
<Icon name="camera-outline" size={18} color={theme.colors.info} />
</View>
<View style={styles.sectionCopy}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>Latest photograph</Text>
<Text style={[styles.sectionSubtitle, { color: theme.colors.textSecondary }]}>
Upload a clear, recent passport-style photo
</Text>
</View>
</View>
<Pressable
onPress={() => setPhotoUploaded((value) => !value)}
style={[
styles.photoTile,
{
backgroundColor: photoUploaded ? theme.tint(theme.colors.success, 0.06) : theme.colors.surfaceMuted,
borderColor: photoUploaded ? theme.tint(theme.colors.success, 0.27) : theme.colors.cardBorder,
},
]}
>
<Icon
name={photoUploaded ? 'checkmark-circle' : 'person-circle-outline'}
size={48}
color={photoUploaded ? theme.colors.success : theme.colors.textMuted}
/>
<Text style={[styles.photoText, { color: photoUploaded ? theme.colors.success : theme.colors.textSecondary }]}>
{photoUploaded ? 'Photograph uploaded' : 'Tap to capture / upload photo'}
</Text>
</Pressable>
</Card>
{/* Self declaration */}
<Card style={styles.sectionCard}>
<View style={styles.sectionHeader}>
<View style={[styles.sectionIcon, { backgroundColor: theme.tint(theme.colors.warning, 0.07) }]}>
<Icon name="document-text-outline" size={18} color={theme.colors.warning} />
</View>
<View style={styles.sectionCopy}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>Self declaration</Text>
<Text style={[styles.sectionSubtitle, { color: theme.colors.textSecondary }]}>
Please read and accept all statements
</Text>
</View>
</View>
{DECLARATIONS.map((statement, index) => {
const accepted = acceptedDeclarations.includes(index);
return (
<Pressable
key={statement}
onPress={() => toggleDeclaration(index)}
style={[
styles.consentCard,
{ backgroundColor: theme.colors.surfaceMuted, borderColor: theme.colors.cardBorder },
]}
>
<Icon
name={accepted ? 'checkmark-circle' : 'ellipse-outline'}
size={22}
color={accepted ? theme.colors.success : theme.colors.textMuted}
/>
<Text style={[styles.consentText, { color: theme.colors.textPrimary }]}>{statement}</Text>
</Pressable>
);
})}
</Card>
{/* Video KYC option */}
<Card style={styles.sectionCard}>
<View style={styles.sectionHeader}>
<View style={[styles.sectionIcon, { backgroundColor: theme.tint(theme.colors.primary, 0.07) }]}>
<Icon name="videocam-outline" size={18} color={theme.colors.primary} />
</View>
<View style={styles.sectionCopy}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>Prefer a video call?</Text>
<Text style={[styles.sectionSubtitle, { color: theme.colors.textSecondary }]}>
Complete re-KYC over a secure video call (V-CIP)
</Text>
</View>
</View>
<Button
title="Start Video KYC"
variant="secondary"
onPress={() => account && navigation.navigate('VideoKyc', { accountNumber: account.accountNumber })}
leftAccessory={<Icon name="videocam-outline" size={18} color={theme.colors.textPrimary} />}
/>
</Card>
<Card style={styles.footerCard}>
<View style={styles.footerHeader}>
<Icon name="time-outline" size={18} color={theme.colors.primary} />
<Text style={[styles.footerTitle, { color: theme.colors.textPrimary }]}>
Processing note
</Text>
</View>
<Text style={[styles.footerText, { color: theme.colors.textSecondary }]}>
Re-KYC updates are usually reviewed within one working day after Aadhaar verification and profile checks.
</Text>
<Button
title="Submit re-KYC request"
onPress={handleSubmit}
leftAccessory={<Icon name="paper-plane-outline" size={18} color={theme.colors.white} />}
/>
</Card>
<View style={{ height: 60 }} />
<BottomSheet
visible={otpSheetVisible}
title="Verify your request"
onClose={() => setOtpSheetVisible(false)}
>
<Text style={{ color: theme.colors.textSecondary, marginBottom: 16, fontSize: 14, lineHeight: 20 }}>
Enter the 6-digit OTP sent to your registered mobile number to confirm this re-KYC request.
</Text>
<Input
label="Enter OTP"
value={otpValue}
onChangeText={setOtpValue}
keyboardType="number-pad"
maxLength={6}
placeholder="000000"
/>
<Button
title="Verify & Submit"
onPress={handleVerifyOtpAndSubmit}
style={{ marginTop: 8 }}
/>
</BottomSheet>
</ScreenShell>
);
}
const styles = StyleSheet.create({
contentContainer: {
gap: 18,
},
heroCard: {
borderRadius: 24,
borderWidth: 1,
padding: 20,
gap: 18,
},
heroHeader: {
flexDirection: 'row',
gap: 12,
},
heroIcon: {
width: 44,
height: 44,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
heroCopy: {
flex: 1,
gap: 4,
},
heroTitle: {
fontSize: 20,
fontFamily: fontFamily.bold,
letterSpacing: -0.4,
},
heroSubtitle: {
fontSize: 13,
lineHeight: 19,
},
stepList: {
gap: 10,
},
stepRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
},
stepIndex: {
width: 28,
height: 28,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
stepIndexText: {
fontSize: 12,
fontFamily: fontFamily.bold,
},
stepText: {
flex: 1,
fontSize: 13,
fontFamily: fontFamily.semibold,
},
sectionCard: {
padding: 20,
gap: 16,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
sectionIcon: {
width: 40,
height: 40,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
sectionCopy: {
flex: 1,
gap: 4,
},
sectionTitle: {
fontSize: 18,
fontFamily: fontFamily.bold,
letterSpacing: -0.3,
},
sectionSubtitle: {
fontSize: 13,
fontFamily: fontFamily.medium,
},
consentCard: {
flexDirection: 'row',
alignItems: 'flex-start',
gap: 12,
borderWidth: 1,
borderRadius: 16,
padding: 16,
},
uploadRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
borderWidth: 1,
borderRadius: 16,
padding: 14,
},
uploadIcon: {
width: 40,
height: 40,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
},
uploadCopy: {
flex: 1,
gap: 2,
},
uploadLabel: {
fontSize: 14,
fontFamily: fontFamily.bold,
},
uploadHint: {
fontSize: 12,
fontFamily: fontFamily.medium,
},
photoTile: {
alignItems: 'center',
justifyContent: 'center',
gap: 10,
borderWidth: 1,
borderStyle: 'dashed',
borderRadius: 16,
paddingVertical: 28,
},
photoText: {
fontSize: 13,
fontFamily: fontFamily.bold,
},
consentText: {
flex: 1,
fontSize: 13,
lineHeight: 19,
fontFamily: fontFamily.medium,
},
chipGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 12,
},
changeChip: {
minWidth: '47%',
flexDirection: 'row',
alignItems: 'center',
gap: 8,
borderWidth: 1,
borderRadius: 16,
paddingHorizontal: 14,
paddingVertical: 12,
},
changeChipText: {
fontSize: 13,
fontFamily: fontFamily.bold,
},
footerCard: {
padding: 20,
gap: 16,
},
footerHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
},
footerTitle: {
fontSize: 16,
fontFamily: fontFamily.bold,
},
footerText: {
fontSize: 13,
lineHeight: 19,
},
emptyCard: {
alignItems: 'center',
paddingVertical: 32,
gap: 12,
},
emptyIcon: {
width: 56,
height: 56,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
},
emptyTitle: {
fontSize: 18,
fontFamily: fontFamily.bold,
},
emptyDescription: {
fontSize: 14,
textAlign: 'center',
lineHeight: 20,
},
});