From ba128a9882a5da6bb4c60901784471c8fc1e6b73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Thu, 22 May 2025 12:24:07 +0200 Subject: [PATCH 1/6] fix: Validate registration number for EU countries --- src/CONST.ts | 30 +++++++++++++++++++ src/libs/ValidationUtils.ts | 24 +++++++++++++++ .../subSteps/RegistrationNumber.tsx | 1 + 3 files changed, 55 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index ef2e43c1ad47..e1048fc1f053 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4088,6 +4088,36 @@ const CONST = { SE: 'Sweden', }, + EU_REGISTRATION_NUMBER_REGEX: { + AT: /^ATU\d{8}$/, + BE: /^BE0\d{9}$/, + BG: /^BG\d{9,10}$/, + HR: /^HR\d{11}$/, + CY: /^CY\d{8}[A-Z]$/, + CZ: /^CZ\d{8,10}$/, + DK: /^DK\d{8}$/, + EE: /^EE\d{9}$/, + FI: /^FI\d{8}$/, + FR: /^FR[0-9A-Z]{2}\d{9}$/, + DE: /^DE\d{9}$/, + GR: /^EL\d{9}$/, + HU: /^HU\d{8}$/, + IE: /^IE\d{7}[A-W][A-I]?$/, + IT: /^IT\d{11}$/, + LT: /^LT(\d{9}|\d{12})$/, + LU: /^LU\d{8}$/, + LV: /^LV\d{11}$/, + MT: /^MT\d{8}$/, + NL: /^NL\d{9}B\d{2}$/, + PL: /^PL\d{10}$/, + PT: /^PT\d{9}$/, + RO: /^RO\d{2,10}$/, + SK: /^SK\d{10}$/, + SI: /^SI\d{8}$/, + ES: /^ES[A-Z0-9]\d{7}[A-Z0-9]$/, + SE: /^SE\d{10}01$/, + }, + 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/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 02cd22ab7c8f..3e34765c3b75 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -637,12 +637,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/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/RegistrationNumber.tsx b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/RegistrationNumber.tsx index 69689fe91147..29504b842206 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/RegistrationNumber.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/RegistrationNumber.tsx @@ -84,6 +84,7 @@ function RegistrationNumber({onNext, isEditing}: RegistrationNumberProps) { {translate('businessInfoStep.whatsThisNumber')} From b3b561b35526a549f10a7356048860646193ffd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Fri, 23 May 2025 12:27:24 +0200 Subject: [PATCH 2/6] lint --- .../NonUSD/BusinessInfo/subSteps/RegistrationNumber.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/RegistrationNumber.tsx b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/RegistrationNumber.tsx index 29504b842206..73b0f343f478 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/RegistrationNumber.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/RegistrationNumber.tsx @@ -29,8 +29,8 @@ function RegistrationNumber({onNext, isEditing}: RegistrationNumberProps) { const styles = useThemeStyles(); const theme = useTheme(); - const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); - const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); + const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: false}); + const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, {canBeMissing: true}); const defaultValue = reimbursementAccount?.achData?.corpay?.[BUSINESS_REGISTRATION_INCORPORATION_NUMBER] ?? ''; const businessStepCountryDraftValue = reimbursementAccount?.achData?.corpay?.[COMPANY_COUNTRY_CODE] ?? reimbursementAccountDraft?.[COMPANY_COUNTRY_CODE] ?? ''; From 8df7aa4e16f2f9f7a712eed6585f589abdcfd131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Tue, 15 Jul 2025 10:23:37 +0200 Subject: [PATCH 3/6] fix: updated validation and added unit test --- src/CONST/index.ts | 54 +++++++++++++++---------------- tests/unit/ValidationUtilsTest.ts | 44 +++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 27 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index b26ddd01f4f6..0f4fa252e6d2 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -4061,33 +4061,33 @@ const CONST = { }, EU_REGISTRATION_NUMBER_REGEX: { - AT: /^ATU\d{8}$/, - BE: /^BE0\d{9}$/, - BG: /^BG\d{9,10}$/, - HR: /^HR\d{11}$/, - CY: /^CY\d{8}[A-Z]$/, - CZ: /^CZ\d{8,10}$/, - DK: /^DK\d{8}$/, - EE: /^EE\d{9}$/, - FI: /^FI\d{8}$/, - FR: /^FR[0-9A-Z]{2}\d{9}$/, - DE: /^DE\d{9}$/, - GR: /^EL\d{9}$/, - HU: /^HU\d{8}$/, - IE: /^IE\d{7}[A-W][A-I]?$/, - IT: /^IT\d{11}$/, - LT: /^LT(\d{9}|\d{12})$/, - LU: /^LU\d{8}$/, - LV: /^LV\d{11}$/, - MT: /^MT\d{8}$/, - NL: /^NL\d{9}B\d{2}$/, - PL: /^PL\d{10}$/, - PT: /^PT\d{9}$/, - RO: /^RO\d{2,10}$/, - SK: /^SK\d{10}$/, - SI: /^SI\d{8}$/, - ES: /^ES[A-Z0-9]\d{7}[A-Z0-9]$/, - SE: /^SE\d{10}01$/, + 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[], diff --git a/tests/unit/ValidationUtilsTest.ts b/tests/unit/ValidationUtilsTest.ts index e9e10dc4f138..bba7dc310e33 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, @@ -495,4 +497,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); + }); + }); + }); }); From cb08d0613e01d6b2f59595e7d70087b41d88560c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Fri, 29 Aug 2025 10:09:31 +0200 Subject: [PATCH 4/6] fix: help link url --- Mobile-Expensify | 2 +- src/CONST/index.ts | 7 +++++++ src/components/SubStepForms/RegistrationNumberStep.tsx | 10 ++++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 29fc015b8f22..a98bfef7f41e 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 29fc015b8f22aebd3072fa7621a41ed116f3ce99 +Subproject commit a98bfef7f41e3856323c249252379f6ac07f9556 diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 9f82f5c88d82..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"}'), diff --git a/src/components/SubStepForms/RegistrationNumberStep.tsx b/src/components/SubStepForms/RegistrationNumberStep.tsx index a8545f8a5943..3fc279f25718 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,12 @@ function RegistrationNumberStep({ const internalInputRef = useRef(null); useDelayedAutoFocus(internalInputRef, shouldDelayAutoFocus); + const helpLink = useMemo(() => { + return Object.prototype.hasOwnProperty.call(CONST.REGISTRATION_NUMBER_HELP_URL, country) + ? 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 +105,7 @@ function RegistrationNumberStep({ {translate('businessInfoStep.whatsThisNumber')} From 3ace17eb37d485a726747584ae4702448e2d6427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Fri, 29 Aug 2025 10:28:10 +0200 Subject: [PATCH 5/6] fix: submodule --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index a98bfef7f41e..29fc015b8f22 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit a98bfef7f41e3856323c249252379f6ac07f9556 +Subproject commit 29fc015b8f22aebd3072fa7621a41ed116f3ce99 From b57939b7e7087310c963f9f33082f578ac18608c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Fri, 29 Aug 2025 11:25:35 +0200 Subject: [PATCH 6/6] fix: simplify condition --- src/components/SubStepForms/RegistrationNumberStep.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/SubStepForms/RegistrationNumberStep.tsx b/src/components/SubStepForms/RegistrationNumberStep.tsx index 3fc279f25718..97c339a82ceb 100644 --- a/src/components/SubStepForms/RegistrationNumberStep.tsx +++ b/src/components/SubStepForms/RegistrationNumberStep.tsx @@ -56,9 +56,7 @@ function RegistrationNumberStep({ useDelayedAutoFocus(internalInputRef, shouldDelayAutoFocus); const helpLink = useMemo(() => { - return Object.prototype.hasOwnProperty.call(CONST.REGISTRATION_NUMBER_HELP_URL, country) - ? CONST.REGISTRATION_NUMBER_HELP_URL[country as keyof typeof CONST.REGISTRATION_NUMBER_HELP_URL] - : CONST.REGISTRATION_NUMBER_HELP_URL.EU; + 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(