diff --git a/src/CONST.ts b/src/CONST.ts index caaa26416e94..de7a6aeca72e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3314,6 +3314,63 @@ const CONST = { ZW: 'Zimbabwe', }, + ALL_EUROPEAN_COUNTRIES: { + AL: 'Albania', + AD: 'Andorra', + AT: 'Austria', + BY: 'Belarus', + BE: 'Belgium', + BA: 'Bosnia & Herzegovina', + BG: 'Bulgaria', + HR: 'Croatia', + CY: 'Cyprus', + CZ: 'Czech Republic', + DK: 'Denmark', + EE: 'Estonia', + FO: 'Faroe Islands', + FI: 'Finland', + FR: 'France', + GE: 'Georgia', + DE: 'Germany', + GI: 'Gibraltar', + GR: 'Greece', + GL: 'Greenland', + HU: 'Hungary', + IS: 'Iceland', + IE: 'Ireland', + IM: 'Isle of Man', + IT: 'Italy', + JE: 'Jersey', + XK: 'Kosovo', + LV: 'Latvia', + LI: 'Liechtenstein', + LT: 'Lithuania', + LU: 'Luxembourg', + MT: 'Malta', + MD: 'Moldova', + MC: 'Monaco', + ME: 'Montenegro', + NL: 'Netherlands', + MK: 'North Macedonia', + NO: 'Norway', + PL: 'Poland', + PT: 'Portugal', + RO: 'Romania', + RU: 'Russia', + SM: 'San Marino', + RS: 'Serbia', + SK: 'Slovakia', + SI: 'Slovenia', + ES: 'Spain', + SJ: 'Svalbard & Jan Mayen', + SE: 'Sweden', + CH: 'Switzerland', + TR: 'Turkey', + UA: 'Ukraine', + GB: 'United Kingdom', + VA: 'Vatican City', + }, + // Sources: https://github.com/Expensify/App/issues/14958#issuecomment-1442138427 // https://github.com/Expensify/App/issues/14958#issuecomment-1456026810 COUNTRY_ZIP_REGEX_DATA: { diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 4ddd816af423..1ba8c273f2a9 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -14,6 +14,7 @@ import type DatePicker from '@components/DatePicker'; import type EmojiPickerButtonDropdown from '@components/EmojiPicker/EmojiPickerButtonDropdown'; import type PercentageForm from '@components/PercentageForm'; import type Picker from '@components/Picker'; +import type PushRowWithModal from '@components/PushRowWithModal'; import type RadioButtons from '@components/RadioButtons'; import type RoomNameInput from '@components/RoomNameInput'; import type SingleChoiceQuestion from '@components/SingleChoiceQuestion'; @@ -63,7 +64,8 @@ type ValidInputs = | typeof NetSuiteMenuWithTopDescriptionForm | typeof CountryPicker | typeof StatePicker - | typeof ConstantSelector; + | typeof ConstantSelector + | typeof PushRowWithModal; type ValueTypeKey = 'string' | 'boolean' | 'date' | 'country' | 'reportFields' | 'disabledListValues'; type ValueTypeMap = { diff --git a/src/components/PushRowWithModal/index.tsx b/src/components/PushRowWithModal/index.tsx index 11c7ff4386d4..65c1969fdf8b 100644 --- a/src/components/PushRowWithModal/index.tsx +++ b/src/components/PushRowWithModal/index.tsx @@ -12,7 +12,7 @@ type PushRowWithModalProps = { selectedOption: string; /** Function to call when the user selects an option */ - onOptionChange: (value: string) => void; + onOptionChange: (option: string) => void; /** Additional styles to apply to container */ wrapperStyles?: StyleProp; @@ -31,6 +31,9 @@ type PushRowWithModalProps = { /** Text to display on error message */ errorText?: string; + + /** Function called whenever option changes */ + onInputChange?: (value: string) => void; }; function PushRowWithModal({ @@ -43,6 +46,7 @@ function PushRowWithModal({ searchInputTitle, shouldAllowChange = true, errorText, + onInputChange = () => {}, }: PushRowWithModalProps) { const [isModalVisible, setIsModalVisible] = useState(false); @@ -56,6 +60,7 @@ function PushRowWithModal({ const handleOptionChange = (value: string) => { onOptionChange(value); + onInputChange(value); }; return ( diff --git a/src/languages/en.ts b/src/languages/en.ts index ea9f0082ebef..e98559c0df93 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2242,6 +2242,10 @@ const translations = { countryStep: { confirmBusinessBank: 'Confirm business bank account currency and country', confirmCurrency: 'Confirm currency and country', + yourBusiness: 'Your business bank account currency must match your workspace currency.', + youCanChange: 'You can change your workspace currency in your', + findCountry: 'Find country', + selectCountry: 'Select country', }, signerInfoStep: { signerInfo: 'Signer info', diff --git a/src/languages/es.ts b/src/languages/es.ts index 236d84aebdd8..a81ded5820bc 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2265,6 +2265,10 @@ const translations = { countryStep: { confirmBusinessBank: 'Confirmar moneda y país de la cuenta bancaria comercial', confirmCurrency: 'Confirmar moneda y país', + yourBusiness: 'La moneda de su cuenta bancaria comercial debe coincidir con la moneda de su espacio de trabajo.', + youCanChange: 'Puede cambiar la moneda de su espacio de trabajo en su', + findCountry: 'Encontrar país', + selectCountry: 'Seleccione su país', }, signerInfoStep: { signerInfo: 'Información del firmante', diff --git a/src/pages/ReimbursementAccount/NonUSD/Country/substeps/Confirmation.tsx b/src/pages/ReimbursementAccount/NonUSD/Country/substeps/Confirmation.tsx index df4cee627c78..28a4485dacfb 100644 --- a/src/pages/ReimbursementAccount/NonUSD/Country/substeps/Confirmation.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/Country/substeps/Confirmation.tsx @@ -1,5 +1,10 @@ -import React, {useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; +import {useOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import PushRowWithModal from '@components/PushRowWithModal'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; import ScrollView from '@components/ScrollView'; @@ -7,19 +12,57 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import mapCurrencyToCountry from '@pages/ReimbursementAccount/utils/mapCurrencyToCountry'; +import * as FormActions from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; + +const {COUNTRY} = INPUT_IDS.ADDITIONAL_DATA; function Confirmation({onNext}: SubStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); + const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); + + const policyID = reimbursementAccount?.achData?.policyID ?? '-1'; + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const currency = policy?.outputCurrency ?? ''; + + const shouldAllowChange = currency === CONST.CURRENCY.EUR; + const currencyMappedToCountry = mapCurrencyToCountry(currency); - const [selectedCountry, setSelectedCountry] = useState(''); + const countryDefaultValue = reimbursementAccount?.achData?.additionalData?.[COUNTRY] ?? reimbursementAccountDraft?.[COUNTRY] ?? ''; + const [selectedCountry, setSelectedCountry] = useState(countryDefaultValue); + + const disableSubmit = !(currency in CONST.CURRENCY); + + const handleSettingsPress = () => { + Navigation.navigate(ROUTES.WORKSPACE_PROFILE.getRoute(policyID)); + }; const handleSelectingCountry = (country: string) => { + FormActions.setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {[COUNTRY]: country}); setSelectedCountry(country); }; + const validate = useCallback((values: FormOnyxValues): FormInputErrors => { + return ValidationUtils.getFieldRequiredErrors(values, [COUNTRY]); + }, []); + + useEffect(() => { + if (currency === CONST.CURRENCY.EUR) { + return; + } + + FormActions.setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {[COUNTRY]: currencyMappedToCountry}); + setSelectedCountry(currencyMappedToCountry); + }, [currency, currencyMappedToCountry]); + return ( {({safeAreaPaddingBottomStyle}) => ( @@ -27,22 +70,45 @@ function Confirmation({onNext}: SubStepProps) { style={styles.pt0} contentContainerStyle={[styles.flexGrow1, safeAreaPaddingBottomStyle]} > + {translate('countryStep.confirmBusinessBank')} + + + {`${translate('countryStep.yourBusiness')} ${translate('countryStep.youCanChange')}`} + + {translate('common.settings').toLowerCase()} + + . + - {translate('countryStep.confirmBusinessBank')} - {/* This is only to showcase usage of PushRowWithModal component. The actual implementation will come in next issue - https://github.com/Expensify/App/issues/50897 */} - diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index 47c1aadf493a..47d30ea20099 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -144,6 +144,7 @@ function ReimbursementAccountPage({route, policy}: ReimbursementAccountPageProps const [onfidoToken = ''] = useOnyx(ONYXKEYS.ONFIDO_TOKEN); const [isLoadingApp = false] = useOnyx(ONYXKEYS.IS_LOADING_APP); const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const [isDebugModeEnabled] = useOnyx(ONYXKEYS.USER, {selector: (user) => !!user?.isDebugModeEnabled}); const policyName = policy?.name ?? ''; const policyIDParam = route.params?.policyID ?? '-1'; @@ -178,7 +179,7 @@ function ReimbursementAccountPage({route, policy}: ReimbursementAccountPageProps const [hasACHDataBeenLoaded, setHasACHDataBeenLoaded] = useState(reimbursementAccount !== CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA && isPreviousPolicy); const [shouldShowContinueSetupButton, setShouldShowContinueSetupButton] = useState(getShouldShowContinueSetupButtonInitialValue()); - function getBankAccountFields(fieldNames: T[]): Pick { + function getBankAccountFields(fieldNames: InputID[]): Partial { return { ...lodashPick(reimbursementAccount?.achData, ...fieldNames), }; @@ -438,8 +439,8 @@ function ReimbursementAccountPage({route, policy}: ReimbursementAccountPageProps const policyCurrency = policy?.outputCurrency ?? ''; // TODO once nonUSD flow is complete update the flag below to reflect all supported currencies, this will be updated in - https://github.com/Expensify/App/issues/50912 const hasUnsupportedCurrency = policyCurrency !== CONST.CURRENCY.USD; - // TODO remove isDevelopment flag once nonUSD flow is complete, this will be updated in - https://github.com/Expensify/App/issues/50912 - const hasForeignCurrency = SUPPORTED_FOREIGN_CURRENCIES.includes(policyCurrency) && isDevelopment; + // TODO remove isDevelopment and isDebugModeEnabled flags once nonUSD flow is complete, this will be updated in - https://github.com/Expensify/App/issues/50912 + const hasForeignCurrency = SUPPORTED_FOREIGN_CURRENCIES.includes(policyCurrency) && (isDevelopment || isDebugModeEnabled); if (userHasPhonePrimaryEmail) { errorText = translate('bankAccount.hasPhoneLoginError'); diff --git a/src/pages/ReimbursementAccount/utils/mapCurrencyToCountry.ts b/src/pages/ReimbursementAccount/utils/mapCurrencyToCountry.ts new file mode 100644 index 000000000000..f4062674c183 --- /dev/null +++ b/src/pages/ReimbursementAccount/utils/mapCurrencyToCountry.ts @@ -0,0 +1,18 @@ +import CONST from '@src/CONST'; + +function mapCurrencyToCountry(currency: string): string { + switch (currency) { + case CONST.CURRENCY.USD: + return CONST.COUNTRY.US; + case CONST.CURRENCY.AUD: + return CONST.COUNTRY.AU; + case CONST.CURRENCY.CAD: + return CONST.COUNTRY.CA; + case CONST.CURRENCY.GBP: + return CONST.COUNTRY.GB; + default: + return ''; + } +} + +export default mapCurrencyToCountry; diff --git a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx index 85d66db58a88..c14f4ea51eab 100644 --- a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx +++ b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx @@ -6,8 +6,12 @@ import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; +import mapCurrencyToCountry from '@pages/ReimbursementAccount/utils/mapCurrencyToCountry'; +import * as FormActions from '@userActions/FormActions'; import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AccessOrNotFoundWrapper from './AccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; @@ -15,10 +19,13 @@ import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; type WorkspaceProfileCurrencyPageProps = WithPolicyAndFullscreenLoadingProps; +const {COUNTRY} = INPUT_IDS.ADDITIONAL_DATA; + function WorkspaceProfileCurrencyPage({policy}: WorkspaceProfileCurrencyPageProps) { const {translate} = useLocalize(); const onSelectCurrency = (item: CurrencyListItem) => { + FormActions.setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {[COUNTRY]: mapCurrencyToCountry(item.currencyCode)}); Policy.updateGeneralSettings(policy?.id ?? '-1', policy?.name ?? '', item.currencyCode); Navigation.setNavigationActionToMicrotaskQueue(Navigation.goBack); }; diff --git a/src/types/form/ReimbursementAccountForm.ts b/src/types/form/ReimbursementAccountForm.ts index c422d3ea7ce4..1d480b993e6a 100644 --- a/src/types/form/ReimbursementAccountForm.ts +++ b/src/types/form/ReimbursementAccountForm.ts @@ -1,3 +1,4 @@ +import type {Country} from '@src/CONST'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import type Form from './Form'; @@ -50,6 +51,9 @@ const INPUT_IDS = { AMOUNT1: 'amount1', AMOUNT2: 'amount2', AMOUNT3: 'amount3', + ADDITIONAL_DATA: { + COUNTRY: 'country', + }, } as const; type InputID = DeepValueOf; @@ -121,8 +125,23 @@ type ReimbursementAccountProps = { [INPUT_IDS.AMOUNT3]: string; }; +/** Additional props for non-USD reimbursement account */ +type NonUSDReimbursementAccountAdditionalProps = { + /** Country of the reimbursement account */ + [INPUT_IDS.ADDITIONAL_DATA.COUNTRY]: Country | ''; +}; + type ReimbursementAccountForm = ReimbursementAccountFormExtraProps & - Form; + Form< + InputID, + BeneficialOwnersStepBaseProps & + BankAccountStepProps & + CompanyStepProps & + RequestorStepProps & + ACHContractStepProps & + ReimbursementAccountProps & + NonUSDReimbursementAccountAdditionalProps + >; export type { ReimbursementAccountForm, @@ -133,6 +152,7 @@ export type { BeneficialOwnersStepProps, ACHContractStepProps, ReimbursementAccountProps, + NonUSDReimbursementAccountAdditionalProps, InputID, }; export default INPUT_IDS; diff --git a/src/types/onyx/ReimbursementAccount.ts b/src/types/onyx/ReimbursementAccount.ts index 0d5c8a83b99b..ad348c5ad390 100644 --- a/src/types/onyx/ReimbursementAccount.ts +++ b/src/types/onyx/ReimbursementAccount.ts @@ -1,6 +1,8 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; +import type {Country} from '@src/CONST'; import type {ACHContractStepProps, BeneficialOwnersStepProps, CompanyStepProps, ReimbursementAccountProps, RequestorStepProps} from '@src/types/form/ReimbursementAccountForm'; +import type INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; import type {BankName} from './Bank'; import type * as OnyxCommon from './OnyxCommon'; @@ -10,6 +12,12 @@ type BankAccountStep = ValueOf; /** Substeps to setup a reimbursement bank account */ type BankAccountSubStep = ValueOf; +/** Additional data where details of the non-USD reimbursements account are stored */ +type AdditionalData = { + /** Country of the reimbursement account */ + [INPUT_IDS.ADDITIONAL_DATA.COUNTRY]: Country | ''; +}; + /** Model of ACH data */ type ACHData = Partial & { /** Step of the setup flow that we are on. Determines which view is presented. */ @@ -50,6 +58,9 @@ type ACHData = Partial