Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"}'),
Expand Down Expand Up @@ -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[],

Expand Down
8 changes: 6 additions & 2 deletions src/components/SubStepForms/RegistrationNumberStep.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -55,6 +55,10 @@ function RegistrationNumberStep<TFormID extends keyof OnyxFormValuesMapping>({
const internalInputRef = useRef<AnimatedTextInputRef>(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<TFormID>): FormInputErrors<TFormID> => {
const errors = getFieldRequiredErrors(values, [inputID]);
Expand Down Expand Up @@ -99,7 +103,7 @@ function RegistrationNumberStep<TFormID extends keyof OnyxFormValuesMapping>({
<View style={[styles.ml2, styles.dFlex, styles.flexRow]}>
<TextLink
style={[styles.textMicro]}
href={CONST.HELP_LINK_URL}
href={helpLink}
>
{translate('businessInfoStep.whatsThisNumber')}
</TextLink>
Expand Down
24 changes: 24 additions & 0 deletions src/libs/ValidationUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
44 changes: 44 additions & 0 deletions tests/unit/ValidationUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -13,6 +14,7 @@ import {
isValidPastDate,
isValidPaymentZipCode,
isValidPersonName,
isValidRegistrationNumber,
isValidRoomName,
isValidTwoFactorCode,
isValidWebsite,
Expand Down Expand Up @@ -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);
});
});
});
});
Loading