diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 0ed5e3b21087..1464eeb276c6 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -950,6 +950,13 @@ const CONST = { SET_NOTIFICATION_LINK: 'https://community.expensify.com/discussion/5651/deep-dive-best-practices-when-youre-running-into-trouble-receiving-emails-from-expensify', GITHUB_URL: 'https://github.com/Expensify/App', HELP_LINK_URL: `${USE_EXPENSIFY_URL}/usa-patriot-act`, + REGISTRATION_NUMBER_HELP_URL: { + AU: 'https://help.expensify.com/articles/new-expensify/wallet-and-payments/Global-Reimbursement-Australia', + CA: 'https://help.expensify.com/articles/new-expensify/wallet-and-payments/Global-Reimbursement-Canada', + EU: 'https://help.expensify.com/articles/new-expensify/wallet-and-payments/Global-Reimbursement-Europe', + UK: 'https://help.expensify.com/articles/new-expensify/wallet-and-payments/Global-Reimbursement-United-Kingdom', + US: 'https://help.expensify.com/articles/new-expensify/wallet-and-payments/Global-Reimbursement-United-States', + }, ELECTRONIC_DISCLOSURES_URL: `${USE_EXPENSIFY_URL}/esignagreement`, GITHUB_RELEASE_URL: 'https://api.github.com/repos/expensify/app/releases/latest', ADD_SECONDARY_LOGIN_URL: encodeURI('settings?param={"section":"account","openModal":"secondaryLogin"}'), @@ -4158,6 +4165,36 @@ const CONST = { SE: 'Sweden', }, + EU_REGISTRATION_NUMBER_REGEX: { + AT: /^FN\d{6}[a-z]?$/i, + BE: /^0\d{3}\.\d{3}\.\d{3}$/, + BG: /^\d{9}|\d{13}$/, + HR: /^\d{11}$/, + CY: /^HE\d{1,6}$/, + CZ: /^\d{8}$/, + DK: /^\d{8}$/, + EE: /^\d{7,8}$/, + FI: /^\d{7}-\d$/, + FR: /^(\d{9}|\d{14})$/, + DE: /^(HRB|HRA)\s?\d+$/, + GR: /^\d{12}$/, + HU: /^\d{2}-\d{6,7}$/, + IE: /^([A-Z]?\d{1,6})$/, + IT: /^(MI-\d+|R\d+|\d{11})$/, + LV: /^\d{11}$/, + LT: /^\d{7}|\d{9}$/, + LU: /^B\d{1,6}$/, + MT: /^C\d+$/, + NL: /^\d{8}$/, + PL: /^\d{10}$/, + PT: /^\d{9}$/, + RO: /^J\d{2}\/\d{4}\/\d+$/, + SK: /^\d{8}$/, + SI: /^\d{8}$/, + ES: /^[A-Z]\d{8}$/, + SE: /^\d{10}$/, + }, + PLAID_EXCLUDED_COUNTRIES: ['IR', 'CU', 'SY', 'UA', 'KP'] as string[], PLAID_SUPPORT_COUNTRIES: ['US', 'CA', 'GB', 'AT', 'BE', 'DK', 'EE', 'FI', 'FR', 'DE', 'IE', 'IT', 'LV', 'LT', 'NL', 'NO', 'PL', 'PT', 'ES', 'SE'] as string[], diff --git a/src/components/SubStepForms/RegistrationNumberStep.tsx b/src/components/SubStepForms/RegistrationNumberStep.tsx index a8545f8a5943..97c339a82ceb 100644 --- a/src/components/SubStepForms/RegistrationNumberStep.tsx +++ b/src/components/SubStepForms/RegistrationNumberStep.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useRef} from 'react'; +import React, {useCallback, useMemo, useRef} from 'react'; import {View} from 'react-native'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; @@ -55,6 +55,10 @@ function RegistrationNumberStep({ const internalInputRef = useRef(null); useDelayedAutoFocus(internalInputRef, shouldDelayAutoFocus); + const helpLink = useMemo(() => { + return CONST.REGISTRATION_NUMBER_HELP_URL[country as keyof typeof CONST.REGISTRATION_NUMBER_HELP_URL] ?? CONST.REGISTRATION_NUMBER_HELP_URL.EU; + }, [country]); + const validate = useCallback( (values: FormOnyxValues): FormInputErrors => { const errors = getFieldRequiredErrors(values, [inputID]); @@ -99,7 +103,7 @@ function RegistrationNumberStep({ {translate('businessInfoStep.whatsThisNumber')} diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index b98a6ebe952c..97b661dbfbff 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -645,12 +645,36 @@ function isValidCARegistrationNumber(registrationNumber: string): boolean { return /^\d{9}(?:[A-Z]{2}\d{4})?$/.test(registrationNumber); } +type EUCountry = keyof typeof CONST.ALL_EUROPEAN_UNION_COUNTRIES; + +/** + * Validates the given value if it is EU member country + * @param country + */ +function isEUMember(country: Country | ''): boolean { + return country in CONST.ALL_EUROPEAN_UNION_COUNTRIES; +} + +/** + * Validates the given values if its is correct registration number for given EU member country + * @param registrationNumber + * @param country + */ +function isValidEURegistrationNumber(registrationNumber: string, country: EUCountry): boolean { + const regex = CONST.EU_REGISTRATION_NUMBER_REGEX[country]; + return !!regex && regex.test(registrationNumber); +} + /** * Validates the given value if it is correct registration number for the given country. * @param registrationNumber * @param country */ function isValidRegistrationNumber(registrationNumber: string, country: Country | '') { + if (isEUMember(country)) { + return isValidEURegistrationNumber(registrationNumber, country as EUCountry); + } + switch (country) { case CONST.COUNTRY.AU: return isValidAURegistrationNumber(registrationNumber); diff --git a/tests/unit/ValidationUtilsTest.ts b/tests/unit/ValidationUtilsTest.ts index 3f10bbccb2ed..1ac8e9b17881 100644 --- a/tests/unit/ValidationUtilsTest.ts +++ b/tests/unit/ValidationUtilsTest.ts @@ -2,6 +2,7 @@ import {addDays, format, startOfDay, subYears} from 'date-fns'; import {TextEncoder} from 'util'; import {translateLocal} from '@libs/Localize'; import CONST from '@src/CONST'; +import type {Country} from '@src/CONST'; import { getAgeRequirementError, isRequiredFulfilled, @@ -13,6 +14,7 @@ import { isValidPastDate, isValidPaymentZipCode, isValidPersonName, + isValidRegistrationNumber, isValidRoomName, isValidTwoFactorCode, isValidWebsite, @@ -501,4 +503,46 @@ describe('ValidationUtils', () => { }); }); }); + + describe('isValidRegistrationNumber', () => { + describe('EU countries', () => { + test.each([ + ['AT', 'FN123456', true], + ['AT', 'FN654321a', true], + ['AT', '123456', false], + ['BE', '0123.456.789', true], + ['BE', '1234.567.890', false], + ['BG', '123456789', true], + ['BG', '1234567890123', true], + ['BG', '12345678', false], + ['DE', 'HRB12345', true], + ['DE', 'HRA 6789', true], + ['DE', 'XYZ123', false], + ['ES', 'A12345678', true], + ['ES', 'B87654321', true], + ['ES', '12345678A', false], + ])('validates EU country registration number', (country, value, expected) => { + expect(isValidRegistrationNumber(value, country as Country)).toBe(expected); + }); + }); + + describe('Non-EU countries', () => { + test.each([ + ['AU', '51824753556', true], + ['AU', '004085616', true], + ['AU', '123456789', false], + ['AU', '51824753557', false], + ['GB', '12345678', true], + ['GB', 'SC123456', true], + ['GB', 'S1234567', false], + ['GB', '1234567A', false], + ['CA', '123456789', true], + ['CA', '123456789RC0001', true], + ['CA', '12345678', false], + ['CA', '123456789XX123', false], + ])('validates Non-EU country registration number', (country, value, expected) => { + expect(isValidRegistrationNumber(value, country as Country)).toBe(expected); + }); + }); + }); });