diff options
author | Sebastian <sebasjm@gmail.com> | 2024-03-08 14:09:31 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-03-08 14:09:31 -0300 |
commit | ddd32a690bd13b1eb1aef1356a1d59fd64e254bf (patch) | |
tree | 44126872f6e8195a3617e2002c696c0afa13fb0d /packages/bank-ui/src/utils.ts | |
parent | e0e82cdf07930d766081e42203c5a4e66d43191f (diff) | |
download | wallet-core-ddd32a690bd13b1eb1aef1356a1d59fd64e254bf.tar.xz |
demobank => bank
Diffstat (limited to 'packages/bank-ui/src/utils.ts')
-rw-r--r-- | packages/bank-ui/src/utils.ts | 447 |
1 files changed, 447 insertions, 0 deletions
diff --git a/packages/bank-ui/src/utils.ts b/packages/bank-ui/src/utils.ts new file mode 100644 index 000000000..8b0febe42 --- /dev/null +++ b/packages/bank-ui/src/utils.ts @@ -0,0 +1,447 @@ +/* + 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 <http://www.gnu.org/licenses/> + */ + +import { + 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<T extends object>(obj: T): T | undefined { + return Object.keys(obj).some( + (k) => (obj as Record<string, T>)[k] !== undefined, + ) + ? obj + : undefined; +} + +export type PartialButDefined<T> = { + [P in keyof T]: T[P] | undefined; +}; + +/** + * every non-map field can be undefined + */ +export type WithIntermediate<Type> = { + [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]> + : Type[prop] | undefined; +}; +export type RecursivePartial<Type> = { + [P in keyof Type]?: Type[P] extends (infer U)[] + ? RecursivePartial<U>[] + : Type[P] extends object + ? RecursivePartial<Type[P]> + : Type[P]; +}; +export type ErrorMessageMappingFor<Type> = { + [prop in keyof Type]+?: Exclude<Type[prop], undefined> extends PaytoString // enumerate known object + ? TranslatedString + : Exclude<Type[prop], undefined> extends AmountString + ? TranslatedString + : Exclude<Type[prop], undefined> extends TranslatedString + ? TranslatedString + : // arrays: every element + Exclude<Type[prop], undefined> extends (infer U)[] + ? ErrorMessageMappingFor<U>[] + : // map: every field + Exclude<Type[prop], undefined> extends object + ? ErrorMessageMappingFor<Type[prop]> + : 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<typeof useTranslationContext>["i18n"]; + +export async function withRuntimeErrorHandling<T>( + i18n: Translator, + cb: () => Promise<T>, +): Promise<void> { + 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), + }; + 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), + }; + 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), + }; + 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), + }; + 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), + }; + break; + } + default: { + result = { + type: "error", + title: i18n.str`Unexpected error`, + description: cause.message as TranslatedString, + debug: JSON.stringify(cause.errorDetail, undefined, 2), + }; + 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 +} + +export function validateRawIBAN( + payto: string, + i18n: InternationalizationAPI, +): TranslatedString | undefined { + return undefined +} + + + +export function validateRawTalerBank( + payto: string, + currentHost: string, + i18n: InternationalizationAPI, +): TranslatedString | undefined { + return undefined +} + |