- 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>
79 lines
2.7 KiB
TypeScript
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;
|