Add TPIN creation, TPIN-authorised transfers, MPIN reset and transfer history

- Create TPIN: current MPIN -> OTP -> encrypted TPIN via /tpin/create/*,
  from Services and from the transfer screen on TPIN_NOT_SET
- IMPS initiate now sends encryptedTpin instead of encryptedMpin; handles
  INVALID_TPIN and TPIN_NOT_SET
- Share one RSA-OAEP encryption and key-retry path for MPIN and TPIN
- Forgot MPIN on sign-in and Reset MPIN in the Security Center
- Fund transfer history list and detail, reachable from Home
- Brand logo on sign-in and palette updates

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Abin Toppo
2026-09-29 17:54:43 +05:30
co-authored by Claude Opus 5.5
parent abb14d5768
commit f1def81eb7
50 changed files with 3039 additions and 385 deletions
@@ -0,0 +1,57 @@
import { isRepeatedDigits, isSequentialDigits } from '../../../utils/validation';
/** A TPIN is always exactly four digits. */
export const TPIN_LENGTH = 4;
/**
* Any four digits — for ENTERING the TPIN on a transfer. Deliberately loose:
* whether they are the right four is the server's to say, with INVALID_TPIN.
*/
export const isValidTpinEntry = (value: string) =>
new RegExp(`^[0-9]{${TPIN_LENGTH}}$`).test(value);
/**
* Inline copy for why a NEW TPIN was refused before it ever left the device —
* for choosing one, not for entering one on a transfer.
*
* `mobileNumber`, when known, adds the "not the last four digits of your
* registered mobile" rule, so the customer is told before a round trip rather
* than after one.
*
* The backend stays the final authority. It also refuses a TPIN matching the
* customer's date of birth, which the app never holds — so that one can only
* arrive as a VALIDATION_ERROR on confirm.
*/
export const describeTpinProblem = (value: string, mobileNumber?: string): string | undefined => {
if (value.length === 0) {
return undefined;
}
if (!/^[0-9]*$/.test(value)) {
return 'Digits only';
}
if (value.length !== TPIN_LENGTH) {
return `A TPIN is ${TPIN_LENGTH} digits`;
}
if (isRepeatedDigits(value)) {
return 'Pick a TPIN that is not the same digit repeated';
}
if (isSequentialDigits(value)) {
return 'Pick a TPIN that is not a run of digits like 1234 or 4321';
}
const mobileDigits = mobileNumber?.replace(/\D/g, '') ?? '';
if (mobileDigits.length >= TPIN_LENGTH && mobileDigits.slice(-TPIN_LENGTH) === value) {
return 'Pick a TPIN that is not the last four digits of your mobile number';
}
return undefined;
};
/** The stricter check for CHOOSING a TPIN. */
export const isAcceptableNewTpin = (value: string, mobileNumber?: string) =>
isValidTpinEntry(value) && describeTpinProblem(value, mobileNumber) === undefined;