/* This file is part of GNU Taler (C) 2022-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see */ import { AbsoluteTime, AmountString, PaytoString, TalerError, TalerErrorCode, TranslatedString, } from "@gnu-taler/taler-util"; import { ErrorNotification, InternationalizationAPI, notify, notifyError, useTranslationContext, } from "@gnu-taler/web-util/browser"; /** * Validate (the number part of) an amount. If needed, * replace comma with a dot. Returns 'false' whenever * the input is invalid, the valid amount otherwise. */ const amountRegex = /^[0-9]+(.[0-9]+)?$/; export function validateAmount( maybeAmount: string | undefined, ): string | undefined { if (!maybeAmount || !amountRegex.test(maybeAmount)) { return; } return maybeAmount; } /** * Extract IBAN from a Payto URI. */ export function getIbanFromPayto(url: string): string { const pathSplit = new URL(url).pathname.split("/"); let lastIndex = pathSplit.length - 1; // Happens if the path ends with "/". if (pathSplit[lastIndex] === "") lastIndex--; const iban = pathSplit[lastIndex]; return iban; } export function undefinedIfEmpty(obj: T): T | undefined { return Object.keys(obj).some( (k) => (obj as Record)[k] !== undefined, ) ? obj : undefined; } export type PartialButDefined = { [P in keyof T]: T[P] | undefined; }; /** * every non-map field can be undefined */ export type WithIntermediate = { [prop in keyof Type]: Type[prop] extends PaytoString ? Type[prop] | undefined : Type[prop] extends AmountString ? Type[prop] | undefined : Type[prop] extends TranslatedString ? Type[prop] | undefined : Type[prop] extends object ? WithIntermediate : Type[prop] | undefined; }; export type RecursivePartial = { [P in keyof Type]?: Type[P] extends (infer U)[] ? RecursivePartial[] : Type[P] extends object ? RecursivePartial : Type[P]; }; export type ErrorMessageMappingFor = { [prop in keyof Type]+?: Exclude extends PaytoString // enumerate known object ? TranslatedString : Exclude extends AmountString ? TranslatedString : Exclude extends TranslatedString ? TranslatedString : // arrays: every element Exclude extends (infer U)[] ? ErrorMessageMappingFor[] : // map: every field Exclude extends object ? ErrorMessageMappingFor : TranslatedString; }; export enum TanChannel { SMS = "sms", EMAIL = "email", } export enum CashoutStatus { // The payment was initiated after a valid // TAN was received by the bank. CONFIRMED = "confirmed", // The cashout was created and now waits // for the TAN by the author. PENDING = "pending", } export const PAGE_SIZE = 5; type Translator = ReturnType["i18n"]; export async function withRuntimeErrorHandling( i18n: Translator, cb: () => Promise, ): Promise { try { await cb(); } catch (error: unknown) { if (error instanceof TalerError) { notify(buildRequestErrorMessage(i18n, error)); } else { notifyError( i18n.str`Operation failed, please report`, (error instanceof Error ? error.message : JSON.stringify(error)) as TranslatedString, ); } } } export function buildRequestErrorMessage( i18n: Translator, cause: TalerError, ): ErrorNotification { let result: ErrorNotification; switch (cause.errorDetail.code) { case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: { result = { type: "error", title: i18n.str`Request timeout`, description: cause.message as TranslatedString, debug: JSON.stringify(cause.errorDetail, undefined, 2), when: AbsoluteTime.now(), }; break; } case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: { result = { type: "error", title: i18n.str`Request throttled`, description: cause.message as TranslatedString, debug: JSON.stringify(cause.errorDetail, undefined, 2), when: AbsoluteTime.now(), }; break; } case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: { result = { type: "error", title: i18n.str`Malformed response`, description: cause.message as TranslatedString, debug: JSON.stringify(cause.errorDetail, undefined, 2), when: AbsoluteTime.now(), }; break; } case TalerErrorCode.WALLET_NETWORK_ERROR: { result = { type: "error", title: i18n.str`Network error`, description: cause.message as TranslatedString, debug: JSON.stringify(cause.errorDetail, undefined, 2), when: AbsoluteTime.now(), }; break; } case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: { result = { type: "error", title: i18n.str`Unexpected request error`, description: cause.message as TranslatedString, debug: JSON.stringify(cause.errorDetail, undefined, 2), when: AbsoluteTime.now(), }; break; } default: { result = { type: "error", title: i18n.str`Unexpected error`, description: cause.message as TranslatedString, debug: JSON.stringify(cause.errorDetail, undefined, 2), when: AbsoluteTime.now(), }; break; } } return result; } export const COUNTRY_TABLE = { AE: "U.A.E.", AF: "Afghanistan", AL: "Albania", AM: "Armenia", AN: "Netherlands Antilles", AR: "Argentina", AT: "Austria", AU: "Australia", AZ: "Azerbaijan", BA: "Bosnia and Herzegovina", BD: "Bangladesh", BE: "Belgium", BG: "Bulgaria", BH: "Bahrain", BN: "Brunei Darussalam", BO: "Bolivia", BR: "Brazil", BT: "Bhutan", BY: "Belarus", BZ: "Belize", CA: "Canada", CG: "Congo", CH: "Switzerland", CI: "Cote d'Ivoire", CL: "Chile", CM: "Cameroon", CN: "People's Republic of China", CO: "Colombia", CR: "Costa Rica", CS: "Serbia and Montenegro", CZ: "Czech Republic", DE: "Germany", DK: "Denmark", DO: "Dominican Republic", DZ: "Algeria", EC: "Ecuador", EE: "Estonia", EG: "Egypt", ER: "Eritrea", ES: "Spain", ET: "Ethiopia", FI: "Finland", FO: "Faroe Islands", FR: "France", GB: "United Kingdom", GD: "Caribbean", GE: "Georgia", GL: "Greenland", GR: "Greece", GT: "Guatemala", HK: "Hong Kong", // HK: "Hong Kong S.A.R.", HN: "Honduras", HR: "Croatia", HT: "Haiti", HU: "Hungary", ID: "Indonesia", IE: "Ireland", IL: "Israel", IN: "India", IQ: "Iraq", IR: "Iran", IS: "Iceland", IT: "Italy", JM: "Jamaica", JO: "Jordan", JP: "Japan", KE: "Kenya", KG: "Kyrgyzstan", KH: "Cambodia", KR: "South Korea", KW: "Kuwait", KZ: "Kazakhstan", LA: "Laos", LB: "Lebanon", LI: "Liechtenstein", LK: "Sri Lanka", LT: "Lithuania", LU: "Luxembourg", LV: "Latvia", LY: "Libya", MA: "Morocco", MC: "Principality of Monaco", MD: "Moldava", // MD: "Moldova", ME: "Montenegro", MK: "Former Yugoslav Republic of Macedonia", ML: "Mali", MM: "Myanmar", MN: "Mongolia", MO: "Macau S.A.R.", MT: "Malta", MV: "Maldives", MX: "Mexico", MY: "Malaysia", NG: "Nigeria", NI: "Nicaragua", NL: "Netherlands", NO: "Norway", NP: "Nepal", NZ: "New Zealand", OM: "Oman", PA: "Panama", PE: "Peru", PH: "Philippines", PK: "Islamic Republic of Pakistan", PL: "Poland", PR: "Puerto Rico", PT: "Portugal", PY: "Paraguay", QA: "Qatar", RE: "Reunion", RO: "Romania", RS: "Serbia", RU: "Russia", RW: "Rwanda", SA: "Saudi Arabia", SE: "Sweden", SG: "Singapore", SI: "Slovenia", SK: "Slovak", SN: "Senegal", SO: "Somalia", SR: "Suriname", SV: "El Salvador", SY: "Syria", TH: "Thailand", TJ: "Tajikistan", TM: "Turkmenistan", TN: "Tunisia", TR: "Turkey", TT: "Trinidad and Tobago", TW: "Taiwan", TZ: "Tanzania", UA: "Ukraine", US: "United States", UY: "Uruguay", VA: "Vatican", VE: "Venezuela", VN: "Viet Nam", YE: "Yemen", ZA: "South Africa", ZW: "Zimbabwe", }; /** * An IBAN is validated by converting it into an integer and performing a * basic mod-97 operation (as described in ISO 7064) on it. * If the IBAN is valid, the remainder equals 1. * * The algorithm of IBAN validation is as follows: * 1.- Check that the total IBAN length is correct as per the country. If not, the IBAN is invalid * 2.- Move the four initial characters to the end of the string * 3.- Replace each letter in the string with two digits, thereby expanding the string, where A = 10, B = 11, ..., Z = 35 * 4.- Interpret the string as a decimal integer and compute the remainder of that number on division by 97 * * If the remainder is 1, the check digit test is passed and the IBAN might be valid. * */ const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; export function validateIBAN( account: string, i18n: InternationalizationAPI, ): TranslatedString | undefined { if (!IBAN_REGEX.test(account)) { return i18n.str`IBAN only have uppercased letters and numbers`; } // Check total length if (account.length < 4) return i18n.str`IBAN numbers have more that 4 digits`; if (account.length > 34) return i18n.str`IBAN numbers have less that 34 digits`; const A_code = "A".charCodeAt(0); const Z_code = "Z".charCodeAt(0); const IBAN = account.toUpperCase(); // check supported country const code = IBAN.substring(0, 2); const found = code in COUNTRY_TABLE; if (!found) return i18n.str`IBAN country code not found`; // 2.- Move the four initial characters to the end of the string const step2 = IBAN.substring(4) + account.substring(0, 4); const step3 = Array.from(step2) .map((letter) => { const code = letter.charCodeAt(0); if (code < A_code || code > Z_code) return letter; return `${letter.charCodeAt(0) - "A".charCodeAt(0) + 10}`; }) .join(""); const checksum = calculate_iban_checksum(step3); if (checksum !== 1) return i18n.str`IBAN number is not valid, checksum is wrong`; return undefined; } function calculate_iban_checksum(str: string): number { const numberStr = str.substring(0, 5); const rest = str.substring(5); const number = parseInt(numberStr, 10); const result = number % 97; if (rest.length > 0) { return calculate_iban_checksum(`${result}${rest}`); } return result; } const USERNAME_REGEX = /^[A-Za-z][A-Za-z0-9]*$/; export function validateTalerBank( account: string, i18n: InternationalizationAPI, ): TranslatedString | undefined { if (!USERNAME_REGEX.test(account)) { return i18n.str`Account only have letters and numbers`; } return undefined; }