Back to Blog

HDFC Bank Frontend System Architecture: Engineering Secure Digital Banking at 90M Customers

May 30, 2026293 min read0 views

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:

ActionPasswordOTPBiometricDevice BindingTransaction 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>
  );
}
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:

  1. Uncompromising security: Multi-factor authentication, encrypted communications, session management
  2. Real-time transactions: Sub-second fund transfers via IMPS/UPI
  3. Regulatory compliance: RBI-mandated session timeouts, password policies, audit trails
  4. Accessibility: WCAG 2.1 AA compliance, multi-language support (15+ languages)
  5. Fraud prevention: Real-time risk scoring, suspicious activity alerts, transaction verification
  6. Performance: Optimized for low-end devices and slow networks common in India
  7. 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?

© 2026 Vidhya Sagar Thakur. All rights reserved.