System Design
Part 8 of 9HDFC Bank Frontend System Architecture: Engineering Secure Digital Banking at 90M Customers
HDFC Bank Frontend System Architecture: Engineering Secure Digital Banking at 90M Customers
1. Product Overview
HDFC Bank operates India's largest private sector digital banking platform, serving 90M+ customers across NetBanking, mobile banking, UPI, and payment gateway services. But the frontend challenge in banking isn't just displaying account balances—it's orchestrating secure multi-factor authentication, real-time transaction processing, regulatory compliance, fraud prevention, and seamless money movement while ensuring zero tolerance for security breaches or financial errors.
Scale assumptions:
- 90M+ retail customers
- 68M+ active digital banking users
- 1.5B+ monthly digital transactions
- ₹4.5L+ Cr monthly transaction value
- 40M+ mobile app users
- 25M+ UPI transactions daily
- 15+ regional language support
- 99.99% uptime requirement (banking SLA)
- Sub-second transaction confirmation
- RBI regulatory compliance (mandatory)
Frontend complexity drivers:
- Security-first architecture: Every interaction requires authentication validation
- Multi-factor authentication: OTP, biometric, device binding, soft tokens
- Transaction signing: Digital signatures for high-value transfers
- Session management: Complex timeout rules, concurrent session handling
- Real-time updates: Balance changes, transaction status, alerts
- Regulatory compliance: RBI guidelines for session timeout, password policy, audit trails
- Accessibility: WCAG 2.1 AA compliance (mandatory for banking)
- Multi-language: Hindi + 14 regional languages + English
- Offline resilience: Critical information accessible without network
- Fraud prevention UI: Real-time risk scoring, suspicious activity alerts
The frontend isn't just a banking interface—it's a security perimeter where every click, every input, and every API call must be validated, logged, and protected against increasingly sophisticated attack vectors.
2. Frontend Challenges
2.1 Security at Every Layer
Challenge: Banking frontends are prime targets for attacks. A single XSS vulnerability could expose millions of accounts.
Attack vectors to defend against:
- Session hijacking: Stealing session tokens
- CSRF attacks: Initiating transactions without user consent
- XSS attacks: Injecting malicious scripts to capture credentials
- Clickjacking: Overlaying invisible frames to capture clicks
- Keylogging: Capturing password/OTP keystrokes
- Man-in-the-middle: Intercepting API communications
- Phishing: Fake login pages stealing credentials
- Credential stuffing: Automated login attempts with leaked passwords
Frontend security measures:
// Security headers (server-side, enforced on every response)
const securityHeaders = {
'Content-Security-Policy': `
default-src 'self';
script-src 'self' 'nonce-{RANDOM}';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://secure.hdfcbank.com;
connect-src 'self' https://api.hdfcbank.com wss://realtime.hdfcbank.com;
frame-ancestors 'none';
form-action 'self';
base-uri 'self';
`,
'X-Frame-Options': 'DENY',
'X-Content-Type-Options': 'nosniff',
'X-XSS-Protection': '1; mode=block',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': 'geolocation=(), microphone=(), camera=()',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
};
2.2 Multi-Factor Authentication Flows
Challenge: Balance security with usability. Too many OTPs = user frustration. Too few = security risk.
Authentication matrix:
| Action | Password | OTP | Biometric | Device Binding | Transaction PIN |
|---|---|---|---|---|---|
| Login (known device) | ✓ | - | Optional | ✓ | - |
| Login (new device) | ✓ | ✓ | - | Register | - |
| View balance | - | - | - | - | - |
| Fund transfer (<₹1L) | - | ✓ | - | ✓ | Optional |
| Fund transfer (>₹1L) | - | ✓ | - | ✓ | ✓ |
| Add beneficiary | - | ✓ | - | ✓ | ✓ |
| Change password | ✓ | ✓ | - | ✓ | - |
| Update mobile | - | ✓ (both) | - | ✓ | ✓ |
State machine for authentication:
type AuthState =
| 'UNAUTHENTICATED'
| 'PASSWORD_VERIFIED'
| 'OTP_PENDING'
| 'OTP_VERIFIED'
| 'BIOMETRIC_PENDING'
| 'FULLY_AUTHENTICATED'
| 'SESSION_EXPIRED'
| 'LOCKED_OUT';
interface AuthContext {
userId: string | null;
sessionId: string | null;
deviceId: string;
authLevel: 'none' | 'basic' | 'elevated' | 'full';
lastActivity: number;
failedAttempts: number;
otpExpiresAt: number | null;
}
type AuthEvent =
| { type: 'SUBMIT_PASSWORD'; password: string }
| { type: 'PASSWORD_SUCCESS'; sessionToken: string }
| { type: 'PASSWORD_FAILURE'; attemptsRemaining: number }
| { type: 'REQUEST_OTP'; channel: 'sms' | 'email' }
| { type: 'SUBMIT_OTP'; otp: string }
| { type: 'OTP_SUCCESS' }
| { type: 'OTP_FAILURE'; attemptsRemaining: number }
| { type: 'OTP_EXPIRED' }
| { type: 'BIOMETRIC_SUCCESS' }
| { type: 'BIOMETRIC_FAILURE' }
| { type: 'SESSION_TIMEOUT' }
| { type: 'LOGOUT' }
| { type: 'ACCOUNT_LOCKED' };
const authMachine = createMachine<AuthContext, AuthEvent>({
id: 'auth',
initial: 'unauthenticated',
context: {
userId: null,
sessionId: null,
deviceId: getDeviceId(),
authLevel: 'none',
lastActivity: Date.now(),
failedAttempts: 0,
otpExpiresAt: null,
},
states: {
unauthenticated: {
on: {
SUBMIT_PASSWORD: 'verifyingPassword',
},
},
verifyingPassword: {
invoke: {
src: 'verifyPassword',
onDone: [
{ target: 'otpPending', cond: 'requiresOTP' },
{ target: 'authenticated', cond: 'noOTPRequired' },
],
onError: [
{ target: 'lockedOut', cond: 'maxAttemptsReached' },
{ target: 'unauthenticated', actions: 'incrementFailedAttempts' },
],
},
},
otpPending: {
entry: 'sendOTP',
on: {
SUBMIT_OTP: 'verifyingOTP',
OTP_EXPIRED: 'otpPending', // Allow resend
RESEND_OTP: { actions: 'resendOTP' },
},
after: {
180000: 'sessionExpired', // 3 minute OTP timeout
},
},
verifyingOTP: {
invoke: {
src: 'verifyOTP',
onDone: 'authenticated',
onError: [
{ target: 'lockedOut', cond: 'maxOTPAttemptsReached' },
{ target: 'otpPending', actions: 'incrementOTPAttempts' },
],
},
},
authenticated: {
entry: ['setSession', 'startActivityMonitor'],
on: {
SESSION_TIMEOUT: 'sessionExpired',
LOGOUT: 'unauthenticated',
},
after: {
300000: 'sessionWarning', // 5 minute inactivity warning
},
},
sessionWarning: {
on: {
ACTIVITY: 'authenticated',
EXTEND_SESSION: 'authenticated',
},
after: {
60000: 'sessionExpired', // 1 minute to respond
},
},
sessionExpired: {
entry: 'clearSession',
on: {
SUBMIT_PASSWORD: 'verifyingPassword',
},
},
lockedOut: {
entry: 'notifyLockout',
// Can only be unlocked by customer service
},
},
});
2.3 Real-Time Balance & Transaction Updates
Scenario: User initiates UPI payment on phone → balance should update on NetBanking within seconds.
Challenges:
- Banking core systems (CBS) have batch processing delays
- Multiple channels (ATM, branch, UPI, NEFT) update same account
- Must show "available balance" vs "ledger balance" correctly
- Transaction status transitions: Pending → Processing → Success/Failed
Real-time architecture:
Transaction initiated (UPI app)
│
├─> Core Banking System processes
│
├─> Event published to message queue
│
├─> WebSocket server receives event
│
└─> Push to all connected sessions for this account
│
├─> NetBanking tab: Update balance
├─> Mobile app: Update balance + notification
└─> SMS: Transaction alert
2.4 Session Management Complexity
RBI mandates:
- Session timeout: Max 5 minutes of inactivity (configurable by bank)
- Concurrent sessions: Must handle or restrict
- Session binding: Tie to IP, device fingerprint, user agent
Implementation:
class SessionManager {
private sessionId: string;
private lastActivity: number;
private warningShown: boolean = false;
private readonly TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
private readonly WARNING_BEFORE_MS = 60 * 1000; // 1 minute warning
constructor() {
this.startActivityMonitor();
this.startHeartbeat();
}
private startActivityMonitor() {
// Track user activity
const activityEvents = ['mousedown', 'keydown', 'touchstart', 'scroll'];
activityEvents.forEach((event) => {
document.addEventListener(event, () => this.recordActivity(), { passive: true });
});
// Check for timeout every 10 seconds
setInterval(() => this.checkTimeout(), 10000);
}
private recordActivity() {
this.lastActivity = Date.now();
this.warningShown = false;
// Reset server-side session timer
this.extendSession();
}
private checkTimeout() {
const elapsed = Date.now() - this.lastActivity;
if (elapsed >= this.TIMEOUT_MS) {
this.handleSessionExpiry();
} else if (elapsed >= this.TIMEOUT_MS - this.WARNING_BEFORE_MS && !this.warningShown) {
this.showTimeoutWarning();
}
}
private showTimeoutWarning() {
this.warningShown = true;
const remainingSeconds = Math.ceil((this.TIMEOUT_MS - (Date.now() - this.lastActivity)) / 1000);
showModal({
title: 'Session Expiring',
message: `Your session will expire in ${remainingSeconds} seconds due to inactivity.`,
actions: [
{ label: 'Continue Session', onClick: () => this.recordActivity() },
{ label: 'Logout', onClick: () => this.logout() },
],
countdown: remainingSeconds,
});
}
private handleSessionExpiry() {
// Clear all sensitive data from memory
this.clearSensitiveData();
// Redirect to login with message
router.push('/login?reason=session_expired');
}
private clearSensitiveData() {
// Clear session storage
sessionStorage.clear();
// Clear any in-memory sensitive data
window.__ACCOUNT_DATA__ = null;
window.__TRANSACTION_DATA__ = null;
// Clear React Query cache
queryClient.clear();
// Clear form data
document.querySelectorAll('input').forEach((input) => {
if (input.type === 'password' || input.name.includes('otp')) {
input.value = '';
}
});
}
private async extendSession() {
// Debounced server call to extend session
await api.post('/session/extend', { sessionId: this.sessionId });
}
private startHeartbeat() {
// Send heartbeat every 60 seconds to keep server session alive
setInterval(async () => {
if (this.isActive()) {
await api.post('/session/heartbeat', { sessionId: this.sessionId });
}
}, 60000);
}
}
2.5 Virtual Keyboard for Password Entry
Requirement: Prevent keylogger attacks by offering on-screen keyboard.
function VirtualKeyboard({ onKeyPress, onBackspace, onClear }: VirtualKeyboardProps) {
// Randomize key positions on each render to prevent position-based attacks
const [keys, setKeys] = useState<string[]>([]);
useEffect(() => {
const allKeys = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
setKeys(shuffleArray(allKeys));
}, []);
return (
<div
className="virtual-keyboard"
onMouseDown={(e) => e.preventDefault()} // Prevent focus loss
>
<div className="keyboard-grid">
{keys.map((key) => (
<button
key={key}
type="button"
onClick={() => onKeyPress(key)}
className="keyboard-key"
>
{key}
</button>
))}
</div>
<div className="keyboard-actions">
<button type="button" onClick={onBackspace}>
← Backspace
</button>
<button type="button" onClick={onClear}>
Clear
</button>
</div>
</div>
);
}
function SecurePasswordInput({ value, onChange }: SecurePasswordInputProps) {
const [showVirtualKeyboard, setShowVirtualKeyboard] = useState(false);
const [maskedValue, setMaskedValue] = useState('');
// Store actual password in memory, not in DOM
const passwordRef = useRef('');
const handleKeyPress = (key: string) => {
passwordRef.current += key;
setMaskedValue('•'.repeat(passwordRef.current.length));
onChange(passwordRef.current);
};
const handleBackspace = () => {
passwordRef.current = passwordRef.current.slice(0, -1);
setMaskedValue('•'.repeat(passwordRef.current.length));
onChange(passwordRef.current);
};
return (
<div className="secure-password-container">
<div className="password-display">
<input
type="text"
value={maskedValue}
readOnly
placeholder="Enter password using keyboard below"
/>
<button
type="button"
onClick={() => setShowVirtualKeyboard(!showVirtualKeyboard)}
>
{showVirtualKeyboard ? 'Hide' : 'Show'} Keyboard
</button>
</div>
{showVirtualKeyboard && (
<VirtualKeyboard
onKeyPress={handleKeyPress}
onBackspace={handleBackspace}
onClear={() => {
passwordRef.current = '';
setMaskedValue('');
onChange('');
}}
/>
)}
</div>
);
}
2.6 Multi-Language Support (15+ Languages)
Requirement: Support English, Hindi, and 14 regional languages (Tamil, Telugu, Kannada, Malayalam, Marathi, Gujarati, Bengali, Punjabi, etc.).
Challenges:
- Right-to-left text (Urdu)
- Different numeral systems (Devanagari numerals)
- Date/time formatting per locale
- Currency formatting (₹ symbol placement)
- Font rendering for complex scripts
// i18n configuration
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
i18n.use(initReactI18next).init({
resources: {
en: {
translation: {
'balance.available': 'Available Balance',
'balance.amount': '₹{{amount}}',
'transfer.title': 'Fund Transfer',
'transfer.confirm': 'Confirm Transfer of ₹{{amount}} to {{beneficiary}}?',
'otp.sent': 'OTP sent to {{maskedMobile}}',
'session.expiring': 'Session expiring in {{seconds}} seconds',
},
},
hi: {
translation: {
'balance.available': 'उपलब्ध शेष',
'balance.amount': '₹{{amount}}',
'transfer.title': 'फंड ट्रांसफर',
'transfer.confirm': '{{beneficiary}} को ₹{{amount}} ट्रांसफर करने की पुष्टि करें?',
'otp.sent': '{{maskedMobile}} पर OTP भेजा गया',
'session.expiring': 'सत्र {{seconds}} सेकंड में समाप्त हो रहा है',
},
},
ta: {
translation: {
'balance.available': 'கிடைக்கும் இருப்பு',
'balance.amount': '₹{{amount}}',
'transfer.title': 'நிதி பரிமாற்றம்',
// ...
},
},
// ... 12 more languages
},
lng: 'en',
fallbackLng: 'en',
interpolation: {
escapeValue: false,
format: (value, format, lng) => {
if (format === 'currency') {
return formatCurrency(value, lng);
}
if (format === 'date') {
return formatDate(value, lng);
}
return value;
},
},
});
// Currency formatting per locale
function formatCurrency(amount: number, locale: string): string {
const formatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'INR',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
return formatter.format(amount);
}
// Usage in component
function AccountBalance({ balance }: { balance: number }) {
const { t, i18n } = useTranslation();
return (
<div className="balance-card">
<span className="balance-label">{t('balance.available')}</span>
<span className="balance-amount">
{formatCurrency(balance, i18n.language)}
</span>
</div>
);
}
3. High-Level Frontend Architecture
3.1 Platform Strategy: Unified Core with Platform Shells
┌────────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ ┌──────────────┬──────────────┬──────────────┬──────────────┐│
│ │ NetBanking │ Mobile App │ Corporate │ PayZapp ││
│ │ (React SPA) │ (React │ NetBanking │ (Mobile) ││
│ │ │ Native) │ (Angular) │ ││
│ └──────┬───────┴──────┬───────┴──────┬───────┴──────┬───────┘│
│ │ │ │ │ │
│ ┌──────▼──────────────▼──────────────▼──────────────▼──────┐ │
│ │ Shared Business Logic Layer │ │
│ │ ┌────────────┬─────────────┬────────────┬─────────────┐ │ │
│ │ │ Auth │ Transaction │ Account │ Validation │ │ │
│ │ │ Service │ Service │ Service │ Rules │ │ │
│ │ └────────────┴─────────────┴────────────┴─────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Security Layer │ │
│ │ ┌──────────┬───────────┬─────────────┬────────────────┐ │ │
│ │ │ Crypto │ Session │ Device │ Fraud │ │ │
│ │ │ Utils │ Manager │ Fingerprint │ Detection │ │ │
│ │ └──────────┴───────────┴─────────────┴────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
3.2 NetBanking Web Architecture
Rendering strategy:
Entry Point Rendering Strategy
───────────────────────────────────────────────────
Login page → SSR (SEO, fast FCP, security headers)
Post-login dashboard → SPA (single session, no page reloads)
Transaction pages → SPA with route guards
Statement download → Server-generated PDF
Public pages (rates) → SSG with ISR
Why SPA for authenticated sections?
- Single session context (no session re-establishment on navigation)
- Faster navigation (no full page reloads)
- Better state management (transaction in progress survives navigation)
- Consistent security context (auth headers, CSRF tokens)
Why SSR for login?
- Security headers must be server-set
- No client-side JavaScript exposure before authentication
- Better control over what's rendered pre-auth
3.3 Application Structure
hdfc-netbanking/
├── apps/
│ ├── netbanking-web/ # Retail NetBanking
│ │ ├── src/
│ │ │ ├── pages/
│ │ │ │ ├── login/
│ │ │ │ ├── dashboard/
│ │ │ │ ├── accounts/
│ │ │ │ ├── transfers/
│ │ │ │ ├── payments/
│ │ │ │ ├── cards/
│ │ │ │ └── services/
│ │ │ ├── components/
│ │ │ ├── hooks/
│ │ │ ├── services/
│ │ │ └── utils/
│ │ └── public/
│ │
│ ├── corporate-banking/ # Corporate NetBanking
│ ├── mobile-app/ # React Native app
│ └── admin-portal/ # Internal admin tools
│
├── packages/
│ ├── @hdfc/ui-components/ # Shared UI components
│ ├── @hdfc/auth/ # Authentication utilities
│ ├── @hdfc/crypto/ # Encryption/decryption
│ ├── @hdfc/validation/ # Form validation rules
│ ├── @hdfc/i18n/ # Internationalization
│ ├── @hdfc/analytics/ # Tracking & logging
│ └── @hdfc/security/ # Security utilities
│
└── shared/
├── types/ # TypeScript definitions
├── constants/ # Bank-wide constants
└── config/ # Environment configs
3.4 Security-First State Management
Principle: Sensitive data should never persist beyond immediate need.
// Zustand store with security-conscious design
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
interface AccountState {
// Non-sensitive: Can be cached
accountList: AccountSummary[];
lastUpdated: number;
// Sensitive: Clear on route change or timeout
selectedAccountDetails: AccountDetails | null;
recentTransactions: Transaction[];
// Highly sensitive: Clear immediately after use
otpValue: string | null;
transactionPin: string | null;
}
interface AccountActions {
setAccounts: (accounts: AccountSummary[]) => void;
setSelectedAccount: (details: AccountDetails) => void;
clearSensitiveData: () => void;
clearHighlySensitiveData: () => void;
}
const useAccountStore = create<AccountState & AccountActions>()(
subscribeWithSelector((set) => ({
accountList: [],
lastUpdated: 0,
selectedAccountDetails: null,
recentTransactions: [],
otpValue: null,
transactionPin: null,
setAccounts: (accounts) => set({ accountList: accounts, lastUpdated: Date.now() }),
setSelectedAccount: (details) => set({ selectedAccountDetails: details }),
clearSensitiveData: () =>
set({
selectedAccountDetails: null,
recentTransactions: [],
}),
clearHighlySensitiveData: () =>
set({
otpValue: null,
transactionPin: null,
}),
}))
);
// Auto-clear highly sensitive data after 30 seconds
useAccountStore.subscribe(
(state) => state.otpValue,
(otpValue) => {
if (otpValue) {
setTimeout(() => {
useAccountStore.getState().clearHighlySensitiveData();
}, 30000);
}
}
);
// Clear sensitive data on route change
router.events.on('routeChangeStart', () => {
useAccountStore.getState().clearSensitiveData();
});
// Clear all data on session end
window.addEventListener('beforeunload', () => {
useAccountStore.getState().clearSensitiveData();
useAccountStore.getState().clearHighlySensitiveData();
});
3.5 API Security Layer
All API calls go through a secure wrapper:
class SecureAPIClient {
private sessionToken: string | null = null;
private csrfToken: string | null = null;
private requestId: number = 0;
async request<T>(config: APIRequestConfig): Promise<T> {
const requestId = ++this.requestId;
// 1. Validate session before request
if (!this.isSessionValid()) {
throw new SessionExpiredError();
}
// 2. Encrypt sensitive request body
const encryptedBody = config.sensitive
? await this.encryptPayload(config.body)
: config.body;
// 3. Add security headers
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-Session-Token': this.sessionToken!,
'X-CSRF-Token': this.csrfToken!,
'X-Request-ID': `${Date.now()}-${requestId}`,
'X-Device-Fingerprint': await this.getDeviceFingerprint(),
'X-Client-Timestamp': Date.now().toString(),
};
// 4. Make request with timeout
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000);
try {
const response = await fetch(config.url, {
method: config.method,
headers,
body: JSON.stringify(encryptedBody),
credentials: 'include',
signal: controller.signal,
});
clearTimeout(timeout);
// 5. Handle security-related status codes
if (response.status === 401) {
this.handleSessionExpiry();
throw new SessionExpiredError();
}
if (response.status === 403) {
throw new ForbiddenError();
}
if (response.status === 429) {
throw new RateLimitError();
}
// 6. Decrypt response if needed
const data = await response.json();
if (config.sensitive) {
return this.decryptPayload(data) as T;
}
return data as T;
} catch (error) {
// 7. Log error securely (no sensitive data)
this.logError(error, { url: config.url, requestId });
throw error;
}
}
private async encryptPayload(payload: unknown): Promise<string> {
const publicKey = await this.getServerPublicKey();
const encrypted = await crypto.subtle.encrypt(
{ name: 'RSA-OAEP' },
publicKey,
new TextEncoder().encode(JSON.stringify(payload))
);
return btoa(String.fromCharCode(...new Uint8Array(encrypted)));
}
private async getDeviceFingerprint(): Promise<string> {
// Collect non-PII device characteristics
const components = [
navigator.userAgent,
navigator.language,
screen.colorDepth,
screen.width + 'x' + screen.height,
new Date().getTimezoneOffset(),
navigator.hardwareConcurrency,
];
const fingerprint = components.join('|');
const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(fingerprint));
return Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
}
4. Fund Transfer Architecture
4.1 Transfer Flow State Machine
type TransferState =
| 'SELECT_MODE' // NEFT/RTGS/IMPS/UPI
| 'SELECT_ACCOUNT' // Source account
| 'SELECT_BENEFICIARY' // Choose or add beneficiary
| 'ENTER_AMOUNT' // Amount and remarks
| 'REVIEW' // Review all details
| 'OTP_VERIFICATION' // Enter OTP
| 'PIN_VERIFICATION' // Enter transaction PIN (for high-value)
| 'PROCESSING' // Server processing
| 'SUCCESS' // Transfer complete
| 'FAILURE' // Transfer failed
| 'TIMEOUT'; // Request timeout
interface TransferContext {
mode: 'NEFT' | 'RTGS' | 'IMPS' | 'UPI' | null;
sourceAccount: Account | null;
beneficiary: Beneficiary | null;
amount: number;
remarks: string;
otpReference: string | null;
transactionId: string | null;
errorMessage: string | null;
}
const transferMachine = createMachine<TransferContext>({
id: 'fundTransfer',
initial: 'selectMode',
context: {
mode: null,
sourceAccount: null,
beneficiary: null,
amount: 0,
remarks: '',
otpReference: null,
transactionId: null,
errorMessage: null,
},
states: {
selectMode: {
on: {
SELECT_NEFT: { target: 'selectAccount', actions: assign({ mode: 'NEFT' }) },
SELECT_RTGS: { target: 'selectAccount', actions: assign({ mode: 'RTGS' }) },
SELECT_IMPS: { target: 'selectAccount', actions: assign({ mode: 'IMPS' }) },
SELECT_UPI: { target: 'selectAccount', actions: assign({ mode: 'UPI' }) },
},
},
selectAccount: {
on: {
SELECT: {
target: 'selectBeneficiary',
actions: assign({ sourceAccount: (_, e) => e.account }),
},
BACK: 'selectMode',
},
},
selectBeneficiary: {
on: {
SELECT: {
target: 'enterAmount',
actions: assign({ beneficiary: (_, e) => e.beneficiary }),
},
ADD_NEW: 'addBeneficiary',
BACK: 'selectAccount',
},
},
addBeneficiary: {
// Nested state machine for adding beneficiary
invoke: {
src: 'addBeneficiaryMachine',
onDone: 'selectBeneficiary',
},
},
enterAmount: {
on: {
SUBMIT: {
target: 'review',
actions: assign({
amount: (_, e) => e.amount,
remarks: (_, e) => e.remarks,
}),
cond: 'isValidAmount',
},
BACK: 'selectBeneficiary',
},
},
review: {
on: {
CONFIRM: 'requestOTP',
MODIFY: 'enterAmount',
BACK: 'enterAmount',
},
},
requestOTP: {
invoke: {
src: 'requestTransferOTP',
onDone: {
target: 'otpVerification',
actions: assign({ otpReference: (_, e) => e.data.otpReference }),
},
onError: {
target: 'failure',
actions: assign({ errorMessage: (_, e) => e.data.message }),
},
},
},
otpVerification: {
on: {
SUBMIT_OTP: [
{ target: 'pinVerification', cond: 'requiresPIN' },
{ target: 'verifyingOTP', cond: 'noPINRequired' },
],
RESEND_OTP: 'requestOTP',
BACK: 'review',
},
after: {
180000: 'timeout', // 3 minute timeout
},
},
verifyingOTP: {
invoke: {
src: 'verifyOTPAndTransfer',
onDone: { target: 'success', actions: assign({ transactionId: (_, e) => e.data.transactionId }) },
onError: { target: 'failure', actions: assign({ errorMessage: (_, e) => e.data.message }) },
},
},
pinVerification: {
on: {
SUBMIT_PIN: 'processing',
BACK: 'otpVerification',
},
},
processing: {
invoke: {
src: 'executeTransfer',
onDone: { target: 'success', actions: assign({ transactionId: (_, e) => e.data.transactionId }) },
onError: { target: 'failure', actions: assign({ errorMessage: (_, e) => e.data.message }) },
},
after: {
60000: 'timeout', // 1 minute processing timeout
},
},
success: {
type: 'final',
entry: 'notifySuccess',
},
failure: {
on: {
RETRY: 'review',
NEW_TRANSFER: 'selectMode',
},
},
timeout: {
on: {
CHECK_STATUS: 'checkingStatus',
NEW_TRANSFER: 'selectMode',
},
},
checkingStatus: {
invoke: {
src: 'checkTransactionStatus',
onDone: [
{ target: 'success', cond: 'isSuccess' },
{ target: 'failure', cond: 'isFailed' },
{ target: 'processing', cond: 'isPending' },
],
},
},
},
});
4.2 Transfer Limits Validation
interface TransferLimits {
dailyLimit: number;
perTransactionLimit: number;
usedToday: number;
remainingDaily: number;
minAmount: number;
cooldownMinutes: number; // For new beneficiaries
}
interface TransferValidation {
isValid: boolean;
errors: ValidationError[];
warnings: ValidationWarning[];
}
function validateTransfer(
amount: number,
mode: TransferMode,
beneficiary: Beneficiary,
limits: TransferLimits
): TransferValidation {
const errors: ValidationError[] = [];
const warnings: ValidationWarning[] = [];
// Amount validation
if (amount <= 0) {
errors.push({ field: 'amount', message: 'Amount must be greater than 0' });
}
if (amount < limits.minAmount) {
errors.push({ field: 'amount', message: `Minimum transfer amount is ₹${limits.minAmount}` });
}
if (amount > limits.perTransactionLimit) {
errors.push({
field: 'amount',
message: `Maximum per-transaction limit is ₹${formatCurrency(limits.perTransactionLimit)}`,
});
}
if (amount > limits.remainingDaily) {
errors.push({
field: 'amount',
message: `Amount exceeds remaining daily limit of ₹${formatCurrency(limits.remainingDaily)}`,
});
}
// Mode-specific validation
if (mode === 'RTGS' && amount < 200000) {
errors.push({ field: 'mode', message: 'RTGS minimum amount is ₹2,00,000' });
}
if (mode === 'NEFT' && amount > 1000000) {
warnings.push({ field: 'mode', message: 'Consider using RTGS for amounts above ₹10,00,000' });
}
// Beneficiary cooldown check
if (beneficiary.addedAt) {
const minutesSinceAdded = (Date.now() - beneficiary.addedAt) / 60000;
if (minutesSinceAdded < limits.cooldownMinutes) {
const remainingMinutes = Math.ceil(limits.cooldownMinutes - minutesSinceAdded);
errors.push({
field: 'beneficiary',
message: `New beneficiary cooldown: ${remainingMinutes} minutes remaining`,
});
}
}
// High-value transfer warning
if (amount > 500000) {
warnings.push({
field: 'amount',
message: 'High-value transfer. Additional verification may be required.',
});
}
return {
isValid: errors.length === 0,
errors,
warnings,
};
}
4.3 OTP Component
function OTPInput({
length = 6,
onComplete,
onResend,
expiresIn,
maskedMobile,
}: OTPInputProps) {
const [otp, setOtp] = useState<string[]>(Array(length).fill(''));
const [remainingTime, setRemainingTime] = useState(expiresIn);
const [isResending, setIsResending] = useState(false);
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
// Countdown timer
useEffect(() => {
const timer = setInterval(() => {
setRemainingTime((prev) => {
if (prev <= 0) {
clearInterval(timer);
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}, []);
// Auto-submit when complete
useEffect(() => {
const completeOtp = otp.join('');
if (completeOtp.length === length && !otp.includes('')) {
onComplete(completeOtp);
}
}, [otp, length, onComplete]);
const handleChange = (index: number, value: string) => {
// Only allow digits
if (value && !/^\d$/.test(value)) return;
const newOtp = [...otp];
newOtp[index] = value;
setOtp(newOtp);
// Auto-focus next input
if (value && index < length - 1) {
inputRefs.current[index + 1]?.focus();
}
};
const handleKeyDown = (index: number, e: React.KeyboardEvent) => {
if (e.key === 'Backspace' && !otp[index] && index > 0) {
// Move to previous input on backspace
inputRefs.current[index - 1]?.focus();
}
};
const handlePaste = (e: React.ClipboardEvent) => {
e.preventDefault();
const pastedData = e.clipboardData.getData('text').slice(0, length);
if (!/^\d+$/.test(pastedData)) return;
const newOtp = [...otp];
pastedData.split('').forEach((char, i) => {
if (i < length) newOtp[i] = char;
});
setOtp(newOtp);
// Focus last filled input
const lastIndex = Math.min(pastedData.length - 1, length - 1);
inputRefs.current[lastIndex]?.focus();
};
const handleResend = async () => {
setIsResending(true);
try {
await onResend();
setRemainingTime(expiresIn);
setOtp(Array(length).fill(''));
inputRefs.current[0]?.focus();
} finally {
setIsResending(false);
}
};
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
return (
<div className="otp-container">
<p className="otp-instruction">
Enter the OTP sent to {maskedMobile}
</p>
<div className="otp-inputs" onPaste={handlePaste}>
{otp.map((digit, index) => (
<input
key={index}
ref={(el) => (inputRefs.current[index] = el)}
type="text"
inputMode="numeric"
maxLength={1}
value={digit}
onChange={(e) => handleChange(index, e.target.value)}
onKeyDown={(e) => handleKeyDown(index, e)}
className="otp-input"
autoComplete="one-time-code"
aria-label={`OTP digit ${index + 1}`}
/>
))}
</div>
<div className="otp-footer">
{remainingTime > 0 ? (
<p className="otp-timer">
OTP expires in <strong>{formatTime(remainingTime)}</strong>
</p>
) : (
<p className="otp-expired">OTP has expired</p>
)}
<button
type="button"
onClick={handleResend}
disabled={remainingTime > 0 || isResending}
className="resend-button"
>
{isResending ? 'Sending...' : 'Resend OTP'}
</button>
</div>
</div>
);
}
5. Real-Time Systems
5.1 WebSocket Architecture for Live Updates
class BankingWebSocket {
private ws: WebSocket | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private heartbeatInterval: NodeJS.Timer | null = null;
private subscriptions: Map<string, Set<(data: unknown) => void>> = new Map();
connect(sessionToken: string) {
const wsUrl = `wss://realtime.hdfcbank.com/ws?token=${sessionToken}`;
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
this.startHeartbeat();
this.resubscribeAll();
};
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
this.handleMessage(message);
};
this.ws.onclose = (event) => {
console.log('WebSocket closed', event.code, event.reason);
this.stopHeartbeat();
if (event.code !== 1000) {
// Abnormal closure, attempt reconnect
this.attemptReconnect(sessionToken);
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error', error);
};
}
private handleMessage(message: WebSocketMessage) {
switch (message.type) {
case 'BALANCE_UPDATE':
this.notifySubscribers('balance', message.data);
break;
case 'TRANSACTION_STATUS':
this.notifySubscribers(`transaction:${message.data.transactionId}`, message.data);
break;
case 'NEW_TRANSACTION':
this.notifySubscribers('transactions', message.data);
break;
case 'ALERT':
this.notifySubscribers('alerts', message.data);
showNotification(message.data);
break;
case 'SESSION_EXPIRED':
this.handleSessionExpiry();
break;
case 'PONG':
// Heartbeat response
break;
}
}
subscribe(channel: string, callback: (data: unknown) => void) {
if (!this.subscriptions.has(channel)) {
this.subscriptions.set(channel, new Set());
}
this.subscriptions.get(channel)!.add(callback);
// Send subscribe message to server
this.send({ type: 'SUBSCRIBE', channel });
return () => {
this.subscriptions.get(channel)?.delete(callback);
if (this.subscriptions.get(channel)?.size === 0) {
this.send({ type: 'UNSUBSCRIBE', channel });
this.subscriptions.delete(channel);
}
};
}
private notifySubscribers(channel: string, data: unknown) {
this.subscriptions.get(channel)?.forEach((callback) => callback(data));
}
private startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
this.send({ type: 'PING' });
}, 30000);
}
private stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
private attemptReconnect(sessionToken: string) {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
showError('Connection lost. Please refresh the page.');
return;
}
const delay = Math.pow(2, this.reconnectAttempts) * 1000; // Exponential backoff
this.reconnectAttempts++;
setTimeout(() => {
this.connect(sessionToken);
}, delay);
}
private send(data: unknown) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
}
// React hook for real-time balance
function useRealTimeBalance(accountId: string) {
const [balance, setBalance] = useState<Balance | null>(null);
const wsClient = useBankingWebSocket();
useEffect(() => {
// Initial fetch
fetchBalance(accountId).then(setBalance);
// Subscribe to updates
const unsubscribe = wsClient.subscribe('balance', (data: BalanceUpdate) => {
if (data.accountId === accountId) {
setBalance((prev) => ({
...prev!,
available: data.availableBalance,
ledger: data.ledgerBalance,
asOf: data.timestamp,
}));
}
});
return unsubscribe;
}, [accountId, wsClient]);
return balance;
}
5.2 Transaction Status Polling (Fallback)
// Fallback when WebSocket unavailable
class TransactionStatusPoller {
private pollingIntervals: Map<string, NodeJS.Timer> = new Map();
startPolling(transactionId: string, onStatusChange: (status: TransactionStatus) => void) {
const poll = async () => {
try {
const status = await api.getTransactionStatus(transactionId);
onStatusChange(status);
// Stop polling if terminal state
if (['SUCCESS', 'FAILED', 'CANCELLED'].includes(status.status)) {
this.stopPolling(transactionId);
}
} catch (error) {
console.error('Polling error', error);
}
};
// Initial poll
poll();
// Poll every 5 seconds
const interval = setInterval(poll, 5000);
this.pollingIntervals.set(transactionId, interval);
// Auto-stop after 5 minutes
setTimeout(() => this.stopPolling(transactionId), 5 * 60 * 1000);
}
stopPolling(transactionId: string) {
const interval = this.pollingIntervals.get(transactionId);
if (interval) {
clearInterval(interval);
this.pollingIntervals.delete(transactionId);
}
}
}
6. Beneficiary Management
6.1 Add Beneficiary Flow
Security requirements:
- OTP verification
- 24-hour cooling period before high-value transfers (configurable)
- Penny-drop verification (optional)
interface BeneficiaryForm {
name: string;
accountNumber: string;
confirmAccountNumber: string;
ifscCode: string;
bankName: string;
branch: string;
nickname: string;
transferLimit: number;
}
const beneficiarySchema = z.object({
name: z.string()
.min(3, 'Name must be at least 3 characters')
.max(50, 'Name cannot exceed 50 characters')
.regex(/^[a-zA-Z\s]+$/, 'Name can only contain letters'),
accountNumber: z.string()
.min(9, 'Account number must be at least 9 digits')
.max(18, 'Account number cannot exceed 18 digits')
.regex(/^\d+$/, 'Account number can only contain digits'),
confirmAccountNumber: z.string(),
ifscCode: z.string()
.length(11, 'IFSC code must be exactly 11 characters')
.regex(/^[A-Z]{4}0[A-Z0-9]{6}$/, 'Invalid IFSC code format'),
bankName: z.string().min(1, 'Bank name is required'),
branch: z.string().min(1, 'Branch is required'),
nickname: z.string()
.max(20, 'Nickname cannot exceed 20 characters')
.optional(),
transferLimit: z.number()
.min(1, 'Transfer limit must be at least ₹1')
.max(10000000, 'Transfer limit cannot exceed ₹1,00,00,000'),
}).refine((data) => data.accountNumber === data.confirmAccountNumber, {
message: 'Account numbers do not match',
path: ['confirmAccountNumber'],
});
function AddBeneficiaryForm() {
const [step, setStep] = useState<'form' | 'verify' | 'otp' | 'success'>('form');
const [formData, setFormData] = useState<BeneficiaryForm | null>(null);
const [bankDetails, setBankDetails] = useState<BankDetails | null>(null);
const { register, handleSubmit, watch, setValue, formState: { errors } } = useForm<BeneficiaryForm>({
resolver: zodResolver(beneficiarySchema),
});
const ifscCode = watch('ifscCode');
// Auto-fetch bank details when IFSC is entered
useEffect(() => {
if (ifscCode?.length === 11) {
fetchBankByIFSC(ifscCode).then((details) => {
if (details) {
setBankDetails(details);
setValue('bankName', details.bankName);
setValue('branch', details.branchName);
}
});
}
}, [ifscCode, setValue]);
const onSubmit = async (data: BeneficiaryForm) => {
setFormData(data);
setStep('verify');
};
const handleVerifyAndProceed = async () => {
// Optional: Penny-drop verification
try {
const verificationResult = await api.verifyAccount({
accountNumber: formData!.accountNumber,
ifscCode: formData!.ifscCode,
});
if (!verificationResult.verified) {
showError('Account verification failed. Please check the details.');
return;
}
} catch (error) {
// Penny-drop failed, show warning but allow to proceed
const proceed = await showConfirmation(
'Account verification unavailable. Do you want to proceed?'
);
if (!proceed) return;
}
// Request OTP
await api.requestBeneficiaryOTP();
setStep('otp');
};
const handleOTPVerified = async (otp: string) => {
await api.addBeneficiary({
...formData!,
otp,
});
setStep('success');
};
return (
<div className="add-beneficiary">
{step === 'form' && (
<form onSubmit={handleSubmit(onSubmit)}>
<FormField label="Beneficiary Name" error={errors.name?.message}>
<input {...register('name')} placeholder="Enter name as per bank records" />
</FormField>
<FormField label="Account Number" error={errors.accountNumber?.message}>
<input
{...register('accountNumber')}
type="password" // Hide account number
placeholder="Enter account number"
/>
</FormField>
<FormField label="Confirm Account Number" error={errors.confirmAccountNumber?.message}>
<input
{...register('confirmAccountNumber')}
type="text"
placeholder="Re-enter account number"
/>
</FormField>
<FormField label="IFSC Code" error={errors.ifscCode?.message}>
<input
{...register('ifscCode')}
placeholder="e.g., HDFC0001234"
style={{ textTransform: 'uppercase' }}
/>
{bankDetails && (
<div className="bank-details-preview">
{bankDetails.bankName}, {bankDetails.branchName}
</div>
)}
</FormField>
<FormField label="Transfer Limit per Transaction" error={errors.transferLimit?.message}>
<CurrencyInput {...register('transferLimit', { valueAsNumber: true })} />
</FormField>
<button type="submit">Continue</button>
</form>
)}
{step === 'verify' && (
<BeneficiaryVerification
data={formData!}
onConfirm={handleVerifyAndProceed}
onBack={() => setStep('form')}
/>
)}
{step === 'otp' && (
<OTPInput
length={6}
onComplete={handleOTPVerified}
onResend={() => api.requestBeneficiaryOTP()}
expiresIn={180}
maskedMobile={getMaskedMobile()}
/>
)}
{step === 'success' && (
<SuccessScreen
title="Beneficiary Added Successfully"
message={`${formData!.name} has been added. You can transfer funds after the cooling period.`}
primaryAction={{ label: 'Make a Transfer', href: '/transfer' }}
secondaryAction={{ label: 'Add Another', onClick: () => setStep('form') }}
/>
)}
</div>
);
}
6.2 IFSC Lookup with Validation
interface IFSCDetails {
bank: string;
branch: string;
address: string;
city: string;
state: string;
contact: string;
upi: boolean;
neft: boolean;
rtgs: boolean;
imps: boolean;
}
async function lookupIFSC(ifscCode: string): Promise<IFSCDetails | null> {
// Validate format first
if (!/^[A-Z]{4}0[A-Z0-9]{6}$/.test(ifscCode)) {
return null;
}
try {
// Check local cache first
const cached = await ifscCache.get(ifscCode);
if (cached) return cached;
// Fetch from API
const response = await api.get(`/ifsc/${ifscCode}`);
if (response.ok) {
const details = await response.json();
// Cache for 30 days
await ifscCache.set(ifscCode, details, 30 * 24 * 60 * 60 * 1000);
return details;
}
return null;
} catch {
return null;
}
}
// Component with debounced lookup
function IFSCInput({ value, onChange, onBankDetailsFound }: IFSCInputProps) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const debouncedLookup = useDebouncedCallback(async (ifsc: string) => {
if (ifsc.length !== 11) return;
setIsLoading(true);
setError(null);
const details = await lookupIFSC(ifsc);
setIsLoading(false);
if (details) {
onBankDetailsFound(details);
} else {
setError('Invalid IFSC code');
}
}, 500);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const ifsc = e.target.value.toUpperCase();
onChange(ifsc);
debouncedLookup(ifsc);
};
return (
<div className="ifsc-input">
<input
type="text"
value={value}
onChange={handleChange}
maxLength={11}
placeholder="HDFC0001234"
className={error ? 'error' : ''}
/>
{isLoading && <Spinner size="small" />}
{error && <span className="error-message">{error}</span>}
</div>
);
}
7. Bill Payments & Recharge
7.1 Biller Discovery and Payment
interface Biller {
id: string;
name: string;
category: BillerCategory;
logo: string;
fields: BillerField[];
fetchBillEnabled: boolean;
partialPaymentEnabled: boolean;
scheduledPaymentEnabled: boolean;
}
interface BillerField {
name: string;
label: string;
type: 'text' | 'number' | 'select' | 'date';
required: boolean;
validation?: {
pattern?: string;
minLength?: number;
maxLength?: number;
options?: { value: string; label: string }[];
};
}
type BillerCategory =
| 'electricity'
| 'water'
| 'gas'
| 'mobile_postpaid'
| 'mobile_prepaid'
| 'broadband'
| 'dth'
| 'insurance'
| 'credit_card'
| 'loan_emi'
| 'municipal_tax'
| 'education';
function BillPaymentFlow() {
const [step, setStep] = useState<'category' | 'biller' | 'details' | 'bill' | 'pay' | 'success'>('category');
const [category, setCategory] = useState<BillerCategory | null>(null);
const [biller, setBiller] = useState<Biller | null>(null);
const [consumerDetails, setConsumerDetails] = useState<Record<string, string>>({});
const [billDetails, setBillDetails] = useState<BillDetails | null>(null);
return (
<div className="bill-payment">
{step === 'category' && (
<CategorySelector
onSelect={(cat) => {
setCategory(cat);
setStep('biller');
}}
/>
)}
{step === 'biller' && (
<BillerSelector
category={category!}
onSelect={(b) => {
setBiller(b);
setStep('details');
}}
onBack={() => setStep('category')}
/>
)}
{step === 'details' && (
<ConsumerDetailsForm
biller={biller!}
onSubmit={async (details) => {
setConsumerDetails(details);
if (biller!.fetchBillEnabled) {
const bill = await fetchBill(biller!.id, details);
setBillDetails(bill);
setStep('bill');
} else {
setStep('pay');
}
}}
onBack={() => setStep('biller')}
/>
)}
{step === 'bill' && (
<BillDetailsView
bill={billDetails!}
biller={biller!}
onPay={(amount) => {
setStep('pay');
}}
onBack={() => setStep('details')}
/>
)}
{step === 'pay' && (
<PaymentConfirmation
biller={biller!}
consumerDetails={consumerDetails}
amount={billDetails?.dueAmount ?? 0}
onSuccess={() => setStep('success')}
onBack={() => setStep('bill')}
/>
)}
{step === 'success' && (
<PaymentSuccess
transactionId={transactionId}
onNewPayment={() => {
setStep('category');
setBiller(null);
setBillDetails(null);
}}
/>
)}
</div>
);
}
7.2 Quick Recharge with Saved Connections
function QuickRecharge() {
const { data: savedConnections } = useQuery({
queryKey: ['savedConnections'],
queryFn: fetchSavedConnections,
});
const { data: recentRecharges } = useQuery({
queryKey: ['recentRecharges'],
queryFn: fetchRecentRecharges,
});
return (
<div className="quick-recharge">
{/* Saved connections */}
<section>
<h3>Your Connections</h3>
<div className="connections-grid">
{savedConnections?.map((conn) => (
<ConnectionCard
key={conn.id}
connection={conn}
onRecharge={() => startRecharge(conn)}
/>
))}
<AddConnectionCard />
</div>
</section>
{/* Recent recharges */}
<section>
<h3>Recent Recharges</h3>
<div className="recent-list">
{recentRecharges?.map((recharge) => (
<RechargeHistoryItem
key={recharge.id}
recharge={recharge}
onRepeat={() => repeatRecharge(recharge)}
/>
))}
</div>
</section>
{/* Browse plans */}
<section>
<h3>Browse Recharge Plans</h3>
<RechargePlanBrowser />
</section>
</div>
);
}
function RechargePlanBrowser() {
const [operator, setOperator] = useState<string>('');
const [circle, setCircle] = useState<string>('');
const [planType, setPlanType] = useState<'topup' | 'validity' | 'data' | 'combo'>('combo');
const { data: plans } = useQuery({
queryKey: ['rechargePlans', operator, circle, planType],
queryFn: () => fetchPlans(operator, circle, planType),
enabled: !!operator && !!circle,
});
return (
<div className="plan-browser">
<div className="filters">
<OperatorSelector value={operator} onChange={setOperator} />
<CircleSelector value={circle} onChange={setCircle} />
<PlanTypeFilter value={planType} onChange={setPlanType} />
</div>
<div className="plans-list">
{plans?.map((plan) => (
<PlanCard
key={plan.id}
plan={plan}
onSelect={() => selectPlan(plan)}
/>
))}
</div>
</div>
);
}
8. Account Statements & Reports
8.1 Statement Download with Filters
function StatementDownload({ accountId }: { accountId: string }) {
const [dateRange, setDateRange] = useState<{ from: Date; to: Date }>({
from: subMonths(new Date(), 1),
to: new Date(),
});
const [format, setFormat] = useState<'pdf' | 'excel' | 'csv'>('pdf');
const [isGenerating, setIsGenerating] = useState(false);
const handleDownload = async () => {
// Validate date range (max 6 months)
const monthsDiff = differenceInMonths(dateRange.to, dateRange.from);
if (monthsDiff > 6) {
showError('Maximum date range is 6 months');
return;
}
setIsGenerating(true);
try {
// Request OTP for statement download (security requirement)
await requestStatementOTP();
const otp = await showOTPModal();
// Generate statement
const response = await api.generateStatement({
accountId,
fromDate: dateRange.from.toISOString(),
toDate: dateRange.to.toISOString(),
format,
otp,
});
// Download file
const blob = await response.blob();
const filename = `Statement_${accountId}_${format(dateRange.from, 'yyyyMMdd')}_${format(dateRange.to, 'yyyyMMdd')}.${format}`;
downloadFile(blob, filename);
// Log download for audit
logAuditEvent('STATEMENT_DOWNLOADED', { accountId, dateRange, format });
} catch (error) {
showError('Failed to generate statement');
} finally {
setIsGenerating(false);
}
};
return (
<div className="statement-download">
<h2>Download Account Statement</h2>
<div className="form-group">
<label>Date Range</label>
<DateRangePicker
value={dateRange}
onChange={setDateRange}
maxDate={new Date()}
minDate={subYears(new Date(), 7)} // 7 years history
maxRange={{ months: 6 }}
/>
</div>
<div className="form-group">
<label>Format</label>
<RadioGroup value={format} onChange={setFormat}>
<RadioButton value="pdf">
PDF
<span className="format-desc">Official statement with letterhead</span>
</RadioButton>
<RadioButton value="excel">
Excel
<span className="format-desc">Editable spreadsheet</span>
</RadioButton>
<RadioButton value="csv">
CSV
<span className="format-desc">For accounting software</span>
</RadioButton>
</RadioGroup>
</div>
<div className="form-group">
<label>Password Protection</label>
<Checkbox
checked={passwordProtect}
onChange={setPasswordProtect}
label="Password protect the statement"
/>
{passwordProtect && (
<p className="help-text">
Statement will be protected with your registered date of birth (DDMMYYYY)
</p>
)}
</div>
<button
onClick={handleDownload}
disabled={isGenerating}
className="download-button"
>
{isGenerating ? 'Generating...' : 'Download Statement'}
</button>
<p className="security-note">
OTP verification required for statement download
</p>
</div>
);
}
8.2 Transaction Search with Filters
function TransactionSearch({ accountId }: { accountId: string }) {
const [filters, setFilters] = useState<TransactionFilters>({
dateRange: { from: subDays(new Date(), 30), to: new Date() },
type: 'all',
amountRange: { min: null, max: null },
searchText: '',
});
const { data, fetchNextPage, hasNextPage, isLoading } = useInfiniteQuery({
queryKey: ['transactions', accountId, filters],
queryFn: ({ pageParam = 1 }) =>
fetchTransactions(accountId, filters, pageParam),
getNextPageParam: (lastPage) => lastPage.nextPage,
});
const transactions = data?.pages.flatMap((page) => page.transactions) ?? [];
return (
<div className="transaction-search">
<div className="filters-panel">
<DateRangePicker
value={filters.dateRange}
onChange={(range) => setFilters({ ...filters, dateRange: range })}
/>
<Select
value={filters.type}
onChange={(type) => setFilters({ ...filters, type })}
>
<option value="all">All Transactions</option>
<option value="credit">Credits Only</option>
<option value="debit">Debits Only</option>
<option value="transfer">Transfers</option>
<option value="bill">Bill Payments</option>
<option value="atm">ATM Withdrawals</option>
</Select>
<AmountRangeInput
value={filters.amountRange}
onChange={(range) => setFilters({ ...filters, amountRange: range })}
/>
<SearchInput
value={filters.searchText}
onChange={(text) => setFilters({ ...filters, searchText: text })}
placeholder="Search by description or reference"
/>
</div>
<div className="transactions-list">
{isLoading && <TransactionsSkeleton />}
{transactions.map((txn) => (
<TransactionRow
key={txn.id}
transaction={txn}
onClick={() => showTransactionDetails(txn)}
/>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} className="load-more">
Load More
</button>
)}
</div>
<TransactionSummary transactions={transactions} />
</div>
);
}
function TransactionRow({ transaction, onClick }: TransactionRowProps) {
const isCredit = transaction.type === 'credit';
return (
<div className="transaction-row" onClick={onClick}>
<div className="txn-icon">
<TransactionIcon type={transaction.category} />
</div>
<div className="txn-details">
<span className="txn-description">{transaction.description}</span>
<span className="txn-date">{formatDate(transaction.date)}</span>
{transaction.reference && (
<span className="txn-reference">Ref: {transaction.reference}</span>
)}
</div>
<div className={`txn-amount ${isCredit ? 'credit' : 'debit'}`}>
{isCredit ? '+' : '-'} {formatCurrency(transaction.amount)}
</div>
<div className="txn-balance">
Balance: {formatCurrency(transaction.runningBalance)}
</div>
</div>
);
}
9. UPI Integration
9.1 UPI Payment Flow
interface UPIPayment {
payeeVPA: string;
payeeName: string;
amount: number;
remarks: string;
transactionId: string;
}
const upiPaymentMachine = createMachine({
id: 'upiPayment',
initial: 'enterVPA',
states: {
enterVPA: {
on: {
SUBMIT_VPA: 'verifyingVPA',
},
},
verifyingVPA: {
invoke: {
src: 'verifyVPA',
onDone: { target: 'enterAmount', actions: 'setPayeeDetails' },
onError: { target: 'enterVPA', actions: 'setVPAError' },
},
},
enterAmount: {
on: {
SUBMIT: 'review',
BACK: 'enterVPA',
},
},
review: {
on: {
CONFIRM: 'pinEntry',
MODIFY: 'enterAmount',
},
},
pinEntry: {
on: {
SUBMIT_PIN: 'processing',
CANCEL: 'review',
},
},
processing: {
invoke: {
src: 'executeUPIPayment',
onDone: 'success',
onError: 'failure',
},
},
success: { type: 'final' },
failure: {
on: {
RETRY: 'pinEntry',
CANCEL: 'enterVPA',
},
},
},
});
function UPIPINInput({ onSubmit, onCancel }: UPIPINInputProps) {
const [pin, setPin] = useState<string[]>(Array(6).fill(''));
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
// Clear PIN on unmount (security)
useEffect(() => {
return () => setPin(Array(6).fill(''));
}, []);
const handleChange = (index: number, value: string) => {
if (!/^\d?$/.test(value)) return;
const newPin = [...pin];
newPin[index] = value;
setPin(newPin);
// Auto-submit when complete
if (newPin.every((d) => d !== '')) {
onSubmit(newPin.join(''));
}
// Move to next input
if (value && index < 5) {
inputRefs.current[index + 1]?.focus();
}
};
return (
<div className="upi-pin-input">
<p>Enter UPI PIN</p>
<div className="pin-inputs">
{pin.map((digit, index) => (
<input
key={index}
ref={(el) => (inputRefs.current[index] = el)}
type="password"
inputMode="numeric"
maxLength={1}
value={digit ? '•' : ''}
onChange={(e) => handleChange(index, e.target.value)}
className="pin-digit"
autoComplete="off"
/>
))}
</div>
<button type="button" onClick={onCancel}>
Cancel
</button>
</div>
);
}
9.2 UPI QR Code Scanner
function UPIQRScanner({ onScan }: { onScan: (data: UPIPaymentData) => void }) {
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
const [error, setError] = useState<string | null>(null);
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
requestCameraPermission();
}, []);
const requestCameraPermission = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' },
});
if (videoRef.current) {
videoRef.current.srcObject = stream;
}
setHasPermission(true);
startScanning();
} catch (err) {
setHasPermission(false);
setError('Camera permission denied');
}
};
const startScanning = async () => {
const barcodeDetector = new (window as any).BarcodeDetector({
formats: ['qr_code'],
});
const scan = async () => {
if (!videoRef.current) return;
try {
const barcodes = await barcodeDetector.detect(videoRef.current);
if (barcodes.length > 0) {
const upiData = parseUPIQRCode(barcodes[0].rawValue);
if (upiData) {
onScan(upiData);
return;
}
}
} catch (err) {
// Continue scanning
}
requestAnimationFrame(scan);
};
scan();
};
return (
<div className="qr-scanner">
{hasPermission === null && <p>Requesting camera permission...</p>}
{hasPermission === false && (
<div className="permission-denied">
<p>{error}</p>
<button onClick={requestCameraPermission}>Try Again</button>
</div>
)}
{hasPermission && (
<>
<video ref={videoRef} autoPlay playsInline />
<div className="scanner-overlay">
<div className="scanner-frame" />
</div>
<p>Position QR code within the frame</p>
</>
)}
</div>
);
}
function parseUPIQRCode(rawValue: string): UPIPaymentData | null {
// UPI QR format: upi://pay?pa=example@upi&pn=Name&am=100&cu=INR&tn=Note
try {
const url = new URL(rawValue);
if (url.protocol !== 'upi:') return null;
const params = new URLSearchParams(url.search);
return {
payeeVPA: params.get('pa') || '',
payeeName: params.get('pn') || '',
amount: parseFloat(params.get('am') || '0'),
currency: params.get('cu') || 'INR',
remarks: params.get('tn') || '',
merchantCode: params.get('mc') || undefined,
};
} catch {
return null;
}
}
10. Fraud Detection UI
10.1 Suspicious Activity Alerts
interface FraudAlert {
id: string;
severity: 'high' | 'medium' | 'low';
type: FraudAlertType;
message: string;
transaction?: Transaction;
detectedAt: Date;
requiresAction: boolean;
actions: FraudAlertAction[];
}
type FraudAlertType =
| 'unusual_location'
| 'high_value_transfer'
| 'new_device'
| 'rapid_transactions'
| 'suspicious_beneficiary'
| 'international_transfer';
function FraudAlertBanner({ alert, onAction, onDismiss }: FraudAlertBannerProps) {
const severityColors = {
high: 'red',
medium: 'orange',
low: 'yellow',
};
return (
<div className={`fraud-alert severity-${alert.severity}`}>
<div className="alert-icon">
<WarningIcon color={severityColors[alert.severity]} />
</div>
<div className="alert-content">
<h4>Security Alert</h4>
<p>{alert.message}</p>
{alert.transaction && (
<div className="alert-transaction">
<span>Amount: {formatCurrency(alert.transaction.amount)}</span>
<span>To: {alert.transaction.beneficiary}</span>
<span>Time: {formatDateTime(alert.transaction.timestamp)}</span>
</div>
)}
</div>
<div className="alert-actions">
{alert.actions.map((action) => (
<button
key={action.type}
onClick={() => onAction(action.type)}
className={`action-${action.type}`}
>
{action.label}
</button>
))}
{!alert.requiresAction && (
<button onClick={onDismiss} className="dismiss">
Dismiss
</button>
)}
</div>
</div>
);
}
// Real-time fraud monitoring
function useFraudMonitoring() {
const [alerts, setAlerts] = useState<FraudAlert[]>([]);
const wsClient = useBankingWebSocket();
useEffect(() => {
const unsubscribe = wsClient.subscribe('fraud_alerts', (alert: FraudAlert) => {
setAlerts((prev) => [alert, ...prev]);
// Show immediate notification for high severity
if (alert.severity === 'high') {
showUrgentNotification(alert);
}
});
return unsubscribe;
}, [wsClient]);
const handleAction = async (alertId: string, action: string) => {
switch (action) {
case 'block_card':
await api.blockCard();
showToast('Card blocked successfully');
break;
case 'report_fraud':
router.push(`/fraud-report?alertId=${alertId}`);
break;
case 'verify_transaction':
await api.verifyTransaction(alertId);
setAlerts((prev) => prev.filter((a) => a.id !== alertId));
break;
case 'not_me':
await api.reportUnauthorizedTransaction(alertId);
showModal({
title: 'Transaction Reported',
message: 'We have blocked your card and will investigate this transaction.',
});
break;
}
};
return { alerts, handleAction };
}
10.2 Transaction Verification Challenge
function TransactionVerificationChallenge({
transaction,
onVerify,
onReject,
}: TransactionVerificationProps) {
const [verificationMethod, setVerificationMethod] = useState<'otp' | 'call' | 'biometric'>('otp');
return (
<div className="verification-challenge">
<div className="challenge-header">
<WarningIcon size="large" />
<h2>Verify This Transaction</h2>
<p>We detected an unusual transaction and need to verify it's you.</p>
</div>
<div className="transaction-details">
<div className="detail-row">
<span>Amount</span>
<span className="amount">{formatCurrency(transaction.amount)}</span>
</div>
<div className="detail-row">
<span>To</span>
<span>{transaction.beneficiary}</span>
</div>
<div className="detail-row">
<span>When</span>
<span>{formatDateTime(transaction.timestamp)}</span>
</div>
<div className="detail-row">
<span>From</span>
<span>{transaction.location || 'Unknown location'}</span>
</div>
</div>
<div className="verification-methods">
<h3>Choose verification method</h3>
<RadioGroup value={verificationMethod} onChange={setVerificationMethod}>
<RadioButton value="otp">
<PhoneIcon />
<span>OTP to registered mobile</span>
</RadioButton>
<RadioButton value="call">
<CallIcon />
<span>Receive a verification call</span>
</RadioButton>
<RadioButton value="biometric">
<FingerprintIcon />
<span>Biometric verification</span>
</RadioButton>
</RadioGroup>
</div>
<div className="challenge-actions">
<button onClick={onVerify} className="verify-button">
Yes, This Was Me
</button>
<button onClick={onReject} className="reject-button">
No, Block This Transaction
</button>
</div>
<p className="help-text">
If you don't recognize this transaction, click "Block" and we'll
immediately secure your account.
</p>
</div>
);
}
11. Accessibility (WCAG 2.1 AA Compliance)
11.1 Accessible Form Components
function AccessibleFormField({
id,
label,
type = 'text',
error,
helpText,
required,
children,
...inputProps
}: AccessibleFormFieldProps) {
const errorId = `${id}-error`;
const helpId = `${id}-help`;
return (
<div className="form-field" role="group" aria-labelledby={`${id}-label`}>
<label id={`${id}-label`} htmlFor={id}>
{label}
{required && <span aria-label="required">*</span>}
</label>
{helpText && (
<p id={helpId} className="help-text">
{helpText}
</p>
)}
{children || (
<input
id={id}
type={type}
aria-required={required}
aria-invalid={!!error}
aria-describedby={`${error ? errorId : ''} ${helpText ? helpId : ''}`.trim() || undefined}
{...inputProps}
/>
)}
{error && (
<p id={errorId} className="error-text" role="alert">
<ErrorIcon aria-hidden="true" />
{error}
</p>
)}
</div>
);
}
// Currency input with screen reader support
function AccessibleCurrencyInput({ value, onChange, label, error }: CurrencyInputProps) {
const [displayValue, setDisplayValue] = useState('');
const formatForDisplay = (amount: number) => {
return new Intl.NumberFormat('en-IN', {
maximumFractionDigits: 2,
minimumFractionDigits: 0,
}).format(amount);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const raw = e.target.value.replace(/[^0-9.]/g, '');
const amount = parseFloat(raw) || 0;
setDisplayValue(raw);
onChange(amount);
};
const handleBlur = () => {
setDisplayValue(formatForDisplay(value));
};
return (
<AccessibleFormField
id="amount"
label={label}
error={error}
helpText="Enter amount in Indian Rupees"
>
<div className="currency-input">
<span className="currency-symbol" aria-hidden="true">₹</span>
<input
id="amount"
type="text"
inputMode="decimal"
value={displayValue}
onChange={handleChange}
onBlur={handleBlur}
aria-label={`${label} in Rupees`}
/>
</div>
<span className="sr-only" aria-live="polite">
Current amount: {formatCurrency(value)}
</span>
</AccessibleFormField>
);
}
11.2 Skip Links and Focus Management
function BankingLayout({ children }: { children: React.ReactNode }) {
const mainRef = useRef<HTMLElement>(null);
return (
<>
{/* Skip links for keyboard navigation */}
<a href="#main-content" className="skip-link">
Skip to main content
</a>
<a href="#navigation" className="skip-link">
Skip to navigation
</a>
<a href="#account-summary" className="skip-link">
Skip to account summary
</a>
<header>
<nav id="navigation" aria-label="Main navigation">
<NavigationMenu />
</nav>
</header>
<aside id="account-summary" aria-label="Account summary">
<AccountSummary />
</aside>
<main id="main-content" ref={mainRef} tabIndex={-1}>
{children}
</main>
<footer>
<FooterLinks />
</footer>
</>
);
}
// Focus management on route change
function useFocusOnRouteChange() {
const pathname = usePathname();
const mainRef = useRef<HTMLElement>(null);
useEffect(() => {
// Announce page change to screen readers
const pageTitle = document.title;
announceToScreenReader(`Navigated to ${pageTitle}`);
// Move focus to main content
mainRef.current?.focus();
}, [pathname]);
return mainRef;
}
function announceToScreenReader(message: string) {
const announcement = document.createElement('div');
announcement.setAttribute('role', 'status');
announcement.setAttribute('aria-live', 'polite');
announcement.setAttribute('aria-atomic', 'true');
announcement.className = 'sr-only';
announcement.textContent = message;
document.body.appendChild(announcement);
setTimeout(() => {
document.body.removeChild(announcement);
}, 1000);
}
11.3 Screen Reader Transaction Announcements
function TransactionList({ transactions }: { transactions: Transaction[] }) {
return (
<div role="region" aria-label="Recent transactions">
<h2 id="transactions-heading">Recent Transactions</h2>
<table aria-labelledby="transactions-heading">
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Description</th>
<th scope="col">Amount</th>
<th scope="col">Balance</th>
</tr>
</thead>
<tbody>
{transactions.map((txn) => (
<tr key={txn.id}>
<td>
<time dateTime={txn.date.toISOString()}>
{formatDate(txn.date)}
</time>
</td>
<td>{txn.description}</td>
<td>
<span
className={txn.type === 'credit' ? 'credit' : 'debit'}
aria-label={`${txn.type === 'credit' ? 'Credit' : 'Debit'} of ${formatCurrency(txn.amount)}`}
>
{txn.type === 'credit' ? '+' : '-'}
{formatCurrency(txn.amount)}
</span>
</td>
<td aria-label={`Balance after transaction: ${formatCurrency(txn.balance)}`}>
{formatCurrency(txn.balance)}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
12. Performance Engineering
12.1 Optimizing for Low-End Devices
Challenge: Many Indian users access banking on budget Android devices with limited RAM and slow CPUs.
// Detect device capabilities
function useDeviceCapabilities() {
const [capabilities, setCapabilities] = useState<DeviceCapabilities>({
isLowEnd: false,
memoryGB: 4,
cpuCores: 4,
connectionType: '4g',
});
useEffect(() => {
const memory = (navigator as any).deviceMemory || 4;
const cpuCores = navigator.hardwareConcurrency || 4;
const connection = (navigator as any).connection?.effectiveType || '4g';
const isLowEnd = memory <= 2 || cpuCores <= 2 || connection === '2g';
setCapabilities({
isLowEnd,
memoryGB: memory,
cpuCores,
connectionType: connection,
});
}, []);
return capabilities;
}
// Conditional rendering based on device
function Dashboard() {
const { isLowEnd } = useDeviceCapabilities();
return (
<div className="dashboard">
<AccountSummary />
{/* Skip animations on low-end devices */}
<QuickActions animated={!isLowEnd} />
{/* Reduce chart complexity on low-end devices */}
{isLowEnd ? (
<SimpleSpendingList />
) : (
<SpendingChart />
)}
{/* Lazy load non-critical sections */}
<Suspense fallback={<SectionSkeleton />}>
<RecentTransactions limit={isLowEnd ? 5 : 10} />
</Suspense>
</div>
);
}
12.2 Bundle Optimization
// Route-based code splitting
const routes = [
{ path: '/dashboard', component: lazy(() => import('./pages/Dashboard')) },
{ path: '/accounts', component: lazy(() => import('./pages/Accounts')) },
{ path: '/transfer', component: lazy(() => import('./pages/Transfer')) },
{ path: '/payments', component: lazy(() => import('./pages/Payments')) },
{ path: '/cards', component: lazy(() => import('./pages/Cards')) },
{ path: '/investments', component: lazy(() => import('./pages/Investments')) },
{ path: '/services', component: lazy(() => import('./pages/Services')) },
];
// Preload critical routes on login
function preloadCriticalRoutes() {
// Dashboard is always needed first
import('./pages/Dashboard');
// Preload after idle
requestIdleCallback(() => {
import('./pages/Accounts');
import('./pages/Transfer');
});
}
// Bundle analysis results:
// Main bundle: 150KB (critical path)
// Dashboard: 80KB
// Transfer: 60KB
// Payments: 45KB
// Cards: 40KB
// Investments: 70KB
// Services: 35KB
12.3 Caching Strategy
// Service worker for offline support
const CACHE_NAME = 'hdfc-netbanking-v1';
const STATIC_ASSETS = [
'/',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.svg',
'/offline.html',
];
const API_CACHE_RULES = {
'/api/accounts': { ttl: 60000, strategy: 'stale-while-revalidate' },
'/api/ifsc/*': { ttl: 86400000, strategy: 'cache-first' }, // 24 hours
'/api/billers': { ttl: 3600000, strategy: 'cache-first' }, // 1 hour
'/api/user/profile': { ttl: 300000, strategy: 'network-first' }, // 5 minutes
};
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Static assets: Cache first
if (STATIC_ASSETS.includes(url.pathname)) {
event.respondWith(cacheFirst(event.request));
return;
}
// API calls: Apply specific strategies
for (const [pattern, config] of Object.entries(API_CACHE_RULES)) {
if (matchPath(url.pathname, pattern)) {
switch (config.strategy) {
case 'cache-first':
event.respondWith(cacheFirst(event.request, config.ttl));
break;
case 'network-first':
event.respondWith(networkFirst(event.request, config.ttl));
break;
case 'stale-while-revalidate':
event.respondWith(staleWhileRevalidate(event.request, config.ttl));
break;
}
return;
}
}
// Default: Network only (for sensitive operations)
event.respondWith(fetch(event.request));
});
13. Architecture Evolution
13.1 Phase 1: Server-Rendered JSP (2000-2010)
Architecture:
- Java Server Pages (JSP)
- Full page reloads for every action
- Limited interactivity
- Basic security (session cookies)
Pain points:
- Slow user experience
- Poor mobile support
- Difficult to maintain
13.2 Phase 2: jQuery + AJAX (2010-2016)
Changes:
- Partial page updates with AJAX
- jQuery for DOM manipulation
- Improved responsiveness
- Basic mobile web support
Improvements:
- Faster interactions
- Better user experience
- Reduced server load
Challenges:
- Spaghetti JavaScript code
- Security vulnerabilities (XSS)
- Difficult to test
13.3 Phase 3: Angular SPA (2016-2020)
Changes:
- Full Angular application
- RESTful API architecture
- Responsive design
- Enhanced security layer
Improvements:
- Modern development experience
- Component-based architecture
- Better maintainability
Challenges:
- Large bundle sizes
- Slow initial load on mobile
- Complex state management
13.4 Phase 4: React + Security-First (2020-Present)
Changes:
- React with TypeScript
- GraphQL for efficient data fetching
- Progressive Web App (PWA)
- Enhanced fraud detection
- Biometric authentication
- Regional language support
Improvements:
- Better performance
- Improved security
- Superior accessibility
- Multi-language support
Current focus:
- AI-powered fraud detection
- Voice banking integration
- WhatsApp banking
- Open banking APIs
14. Regulatory Compliance UI
14.1 RBI Compliance Features
// Session timeout per RBI guidelines
const RBI_SESSION_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
// Password policy per RBI guidelines
const RBI_PASSWORD_POLICY = {
minLength: 8,
maxLength: 28,
requireUppercase: true,
requireLowercase: true,
requireNumber: true,
requireSpecialChar: true,
preventReuse: 3, // Cannot reuse last 3 passwords
expiryDays: 90, // Must change every 90 days
maxFailedAttempts: 3, // Lock after 3 failed attempts
};
// Transaction limits per RBI guidelines
const RBI_TRANSACTION_LIMITS = {
IMPS: {
perTransaction: 500000, // ₹5 lakhs
daily: 1000000, // ₹10 lakhs
},
NEFT: {
perTransaction: null, // No limit
daily: null,
},
UPI: {
perTransaction: 100000, // ₹1 lakh (standard)
daily: 100000,
},
};
// Mandatory disclosures
function TransactionDisclosure({ type, amount }: TransactionDisclosureProps) {
return (
<div className="transaction-disclosure" role="region" aria-label="Important information">
<h4>Important Information</h4>
<ul>
<li>
This transaction is processed through {type} system governed by RBI.
</li>
<li>
Transaction charges, if any, will be debited from your account.
</li>
<li>
Funds will be credited to beneficiary account as per {type} settlement cycle.
</li>
{amount > 1000000 && (
<li>
Transactions above ₹10 lakhs may be reported under CTR (Cash Transaction Report).
</li>
)}
</ul>
<p className="consent-text">
By proceeding, you confirm that the transaction details are correct and
you authorize HDFC Bank to process this transaction.
</p>
</div>
);
}
14.2 Audit Trail
// Client-side audit logging
class AuditLogger {
private logs: AuditLog[] = [];
private batchSize = 10;
log(event: AuditEvent) {
const log: AuditLog = {
id: crypto.randomUUID(),
timestamp: Date.now(),
sessionId: getSessionId(),
userId: getUserId(),
event: event.type,
details: event.details,
deviceFingerprint: getDeviceFingerprint(),
ipAddress: null, // Set by server
location: null, // Set by server
};
this.logs.push(log);
// Batch send to server
if (this.logs.length >= this.batchSize) {
this.flush();
}
}
async flush() {
if (this.logs.length === 0) return;
const logsToSend = [...this.logs];
this.logs = [];
try {
await api.post('/audit/logs', { logs: logsToSend });
} catch {
// Re-queue on failure
this.logs = [...logsToSend, ...this.logs];
}
}
}
// Usage
const auditLogger = new AuditLogger();
// Log all significant actions
auditLogger.log({ type: 'LOGIN_ATTEMPT', details: { method: 'password' } });
auditLogger.log({ type: 'TRANSFER_INITIATED', details: { amount: 50000, beneficiary: 'XXX' } });
auditLogger.log({ type: 'OTP_VERIFIED', details: { action: 'transfer' } });
auditLogger.log({ type: 'STATEMENT_DOWNLOADED', details: { format: 'pdf', dateRange: '...' } });
15. Future Architecture
15.1 Voice Banking Integration
// Voice command recognition for banking
class VoiceBankingController {
private recognition: SpeechRecognition;
private synthesis: SpeechSynthesis;
async processCommand(transcript: string) {
const intent = await classifyIntent(transcript);
switch (intent.type) {
case 'CHECK_BALANCE':
const balance = await api.getBalance();
this.speak(`Your available balance is ${formatCurrency(balance)}`);
break;
case 'TRANSFER_MONEY':
// Confirm details verbally
this.speak(`You want to transfer ${intent.amount} to ${intent.beneficiary}. Is that correct?`);
const confirmed = await this.listenForConfirmation();
if (confirmed) {
// Initiate transfer flow
router.push(`/transfer?prefill=${encodeURIComponent(JSON.stringify(intent))}`);
}
break;
case 'RECENT_TRANSACTIONS':
const transactions = await api.getRecentTransactions(5);
this.speakTransactions(transactions);
break;
}
}
}
15.2 WhatsApp Banking
// WhatsApp Business API integration
interface WhatsAppBankingMessage {
from: string; // WhatsApp number
type: 'text' | 'interactive' | 'template';
content: string | InteractiveMessage;
}
// Frontend component for WhatsApp-style chat
function WhatsAppBankingChat() {
const [messages, setMessages] = useState<ChatMessage[]>([]);
return (
<div className="whatsapp-banking-chat">
<div className="chat-header">
<img src="/hdfc-logo.png" alt="HDFC Bank" />
<span>HDFC Bank</span>
<VerifiedBadge />
</div>
<div className="chat-messages">
{messages.map((msg) => (
<ChatBubble key={msg.id} message={msg} />
))}
</div>
<div className="quick-actions">
<QuickActionButton
icon={<BalanceIcon />}
label="Check Balance"
onClick={() => sendMessage('Check balance')}
/>
<QuickActionButton
icon={<TransferIcon />}
label="Transfer"
onClick={() => sendMessage('Transfer money')}
/>
<QuickActionButton
icon={<StatementIcon />}
label="Mini Statement"
onClick={() => sendMessage('Mini statement')}
/>
</div>
</div>
);
}
15.3 Open Banking / Account Aggregator
// Account Aggregator consent management
interface AAConsent {
id: string;
fiuName: string; // Financial Information User
purpose: string;
dataTypes: DataType[];
frequency: 'one_time' | 'recurring';
dateRange: { from: Date; to: Date };
status: 'pending' | 'approved' | 'rejected' | 'revoked';
}
function AccountAggregatorConsent({ consent }: { consent: AAConsent }) {
return (
<div className="aa-consent-card">
<div className="consent-header">
<img src={consent.fiuLogo} alt={consent.fiuName} />
<h3>{consent.fiuName}</h3>
<span className={`status-${consent.status}`}>{consent.status}</span>
</div>
<div className="consent-details">
<p><strong>Purpose:</strong> {consent.purpose}</p>
<p><strong>Data requested:</strong></p>
<ul>
{consent.dataTypes.map((type) => (
<li key={type}>{DATA_TYPE_LABELS[type]}</li>
))}
</ul>
<p><strong>Duration:</strong> {formatDateRange(consent.dateRange)}</p>
<p><strong>Frequency:</strong> {consent.frequency === 'one_time' ? 'One time' : 'Recurring'}</p>
</div>
{consent.status === 'pending' && (
<div className="consent-actions">
<button onClick={() => approveConsent(consent.id)} className="approve">
Approve
</button>
<button onClick={() => rejectConsent(consent.id)} className="reject">
Reject
</button>
</div>
)}
{consent.status === 'approved' && (
<button onClick={() => revokeConsent(consent.id)} className="revoke">
Revoke Consent
</button>
)}
</div>
);
}
Conclusion
HDFC Bank's frontend architecture is a masterclass in security-first digital banking at scale. The system serves 68M+ digital banking users while maintaining:
- Uncompromising security: Multi-factor authentication, encrypted communications, session management
- Real-time transactions: Sub-second fund transfers via IMPS/UPI
- Regulatory compliance: RBI-mandated session timeouts, password policies, audit trails
- Accessibility: WCAG 2.1 AA compliance, multi-language support (15+ languages)
- Fraud prevention: Real-time risk scoring, suspicious activity alerts, transaction verification
- Performance: Optimized for low-end devices and slow networks common in India
- Seamless UX: Complex flows (transfers, beneficiary management) made intuitive
Key engineering principles:
- Security is non-negotiable: Every feature designed with security first
- Trust through transparency: Clear disclosures, audit trails, consent management
- Inclusive design: Works for all users regardless of device, language, or ability
- Regulatory alignment: Built to comply with RBI and data protection regulations
- Resilience: Graceful degradation, offline support, error recovery
The future points toward voice banking, WhatsApp integration, Account Aggregator participation, and AI-powered fraud detection—but the foundational principle remains: protect customer assets and data while delivering seamless banking experiences.
HDFC Bank's frontend isn't just displaying financial information. It's guarding customer wealth while enabling digital transformation—and that requires engineering excellence at every layer.
Engineering banking systems is about building trust. HDFC Bank's frontend architecture demonstrates that security, compliance, and great user experience can coexist at massive scale.
What did you think?