summaryrefslogtreecommitdiff
path: root/packages/bank-ui/src/utils.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/bank-ui/src/utils.ts')
-rw-r--r--packages/bank-ui/src/utils.ts447
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
+}
+