Files
CBS-Mobile-App/src/modules/payments/utils/tpinValidation.ts
T
Abin ToppoandClaude Opus 5.5 5fbd13ba12 Add TPIN reset via OTP, with server-clock countdowns and redacted API logs
- Reset TPIN: empty-body initiate -> OTP -> encrypted new TPIN via
  /tpin/reset/*, from a Services tile, search, and "Forgot your TPIN?"
  on the transfer's TPIN sheet (returns to the same transfer review)
- Handle every documented code: SESSION_INVALID signs out, TPIN_NOT_SET
  offers Create TPIN, CBS customer/account/org refusals stop the flow,
  outages retry from the start, expired or spent OTP/token restarts,
  policy VALIDATION_ERROR stays on the new-TPIN step
- Secrets live in screen state only and are dropped as each step ends
- Countdowns use the envelope timestamp, kept aside by the interceptor,
  so a wrong device clock cannot skew OTP or token expiry
- New TPINs refuse any three-digit run (1235, 5987), for create too
- Dev API logger redacts the session token, OTPs, PINs, encrypted
  payloads and one-time tokens
- Tests: request bodies, RSA-OAEP SHA-256 payload decrypted with Node,
  fresh nonce on key retry, server-clock deadlines, log redaction

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:41:03 +05:30

79 lines
2.7 KiB
TypeScript

import { isRepeatedDigits, isSequentialDigits } from '../../../utils/validation';
/** A TPIN is always exactly four digits. */
export const TPIN_LENGTH = 4;
/** The shortest run up or down the backend's TPIN policy refuses. */
const SEQUENCE_RUN_LENGTH = 3;
/**
* True when any three neighbouring digits run straight up or down — 1235 (123),
* 5987 (987) and 1234 alike. The TPIN policy refuses "a sequence of three
* digits" and answers "TPIN cannot contain sequential digits", so a run
* anywhere counts, not only one spanning all four. No wrap-around: 8901 is not
* a run.
*/
export const containsSequentialRun = (value: string) => {
for (let start = 0; start + SEQUENCE_RUN_LENGTH <= value.length; start += 1) {
if (isSequentialDigits(value.slice(start, start + SEQUENCE_RUN_LENGTH))) {
return true;
}
}
return false;
};
/**
* 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, and — on a reset — the
* current TPIN or any of the previous three, which only the server's history
* knows. Those 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 (containsSequentialRun(value)) {
return 'Pick a TPIN without a run of digits like 123 or 987';
}
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;