From cd8ca07937025d621e7fb6802526e2a64b08aff5 Mon Sep 17 00:00:00 2001 From: Yogesh Bhatt Date: Sat, 7 Oct 2023 12:53:52 +0530 Subject: [PATCH 0001/1082] Convert StatePicker modal to StateSelection page --- src/ROUTES.ts | 16 ++ .../StatePicker/StateSelectorModal.js | 112 -------------- src/components/StatePicker/index.js | 97 ------------- src/components/StateSelector.js | 87 +++++++++++ .../AppNavigator/ModalStackNavigators.js | 1 + src/libs/Navigation/linkingConfig.js | 4 + src/libs/getStateFromRoute.ts | 13 ++ src/pages/ReimbursementAccount/AddressForm.js | 4 +- src/pages/ReimbursementAccount/CompanyStep.js | 23 ++- .../ReimbursementAccountPage.js | 1 + .../Profile/PersonalDetails/AddressPage.js | 22 ++- .../PersonalDetails/StateSelectionPage.js | 137 ++++++++++++++++++ src/pages/settings/Wallet/AddDebitCardPage.js | 19 ++- src/stories/Form.stories.js | 6 +- 14 files changed, 317 insertions(+), 225 deletions(-) delete mode 100644 src/components/StatePicker/StateSelectorModal.js delete mode 100644 src/components/StatePicker/index.js create mode 100644 src/components/StateSelector.js create mode 100644 src/libs/getStateFromRoute.ts create mode 100644 src/pages/settings/Profile/PersonalDetails/StateSelectionPage.js diff --git a/src/ROUTES.ts b/src/ROUTES.ts index b2dafa643b22..642e8ec8a2d0 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -94,6 +94,22 @@ export default { return route; }, }, + SETTINGS_PERSONAL_DETAILS_ADDRESS_STATE: { + route: 'settings/profile/personal-details/address/state', + getRoute: (state: string, backTo?: string, label?: string, stateParamName = 'state') => { + let route = `settings/profile/personal-details/address/state?state=${state}`; + if (backTo) { + route += `&backTo=${encodeURIComponent(backTo)}`; + } + if (label) { + route += `&label=${encodeURIComponent(label)}`; + } + if (stateParamName) { + route += `&stateParamName=${encodeURIComponent(stateParamName)}`; + } + return route; + }, + }, SETTINGS_CONTACT_METHODS: 'settings/profile/contact-methods', SETTINGS_CONTACT_METHOD_DETAILS: { route: 'settings/profile/contact-methods/:contactMethod/details', diff --git a/src/components/StatePicker/StateSelectorModal.js b/src/components/StatePicker/StateSelectorModal.js deleted file mode 100644 index 378dcc4ebc8b..000000000000 --- a/src/components/StatePicker/StateSelectorModal.js +++ /dev/null @@ -1,112 +0,0 @@ -import _ from 'underscore'; -import React, {useMemo, useEffect} from 'react'; -import PropTypes from 'prop-types'; -import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; -import CONST from '../../CONST'; -import Modal from '../Modal'; -import HeaderWithBackButton from '../HeaderWithBackButton'; -import SelectionList from '../SelectionList'; -import useLocalize from '../../hooks/useLocalize'; -import ScreenWrapper from '../ScreenWrapper'; -import styles from '../../styles/styles'; -import searchCountryOptions from '../../libs/searchCountryOptions'; -import StringUtils from '../../libs/StringUtils'; - -const propTypes = { - /** Whether the modal is visible */ - isVisible: PropTypes.bool.isRequired, - - /** State value selected */ - currentState: PropTypes.string, - - /** Function to call when the user selects a State */ - onStateSelected: PropTypes.func, - - /** Function to call when the user closes the State modal */ - onClose: PropTypes.func, - - /** The search value from the selection list */ - searchValue: PropTypes.string.isRequired, - - /** Function to call when the user types in the search input */ - setSearchValue: PropTypes.func.isRequired, - - /** Label to display on field */ - label: PropTypes.string, -}; - -const defaultProps = { - currentState: '', - onClose: () => {}, - onStateSelected: () => {}, - label: undefined, -}; - -function StateSelectorModal({currentState, isVisible, onClose, onStateSelected, searchValue, setSearchValue, label}) { - const {translate} = useLocalize(); - - useEffect(() => { - if (isVisible) { - return; - } - setSearchValue(''); - }, [isVisible, setSearchValue]); - - const countryStates = useMemo( - () => - _.map(_.keys(COMMON_CONST.STATES), (state) => { - const stateName = translate(`allStates.${state}.stateName`); - const stateISO = translate(`allStates.${state}.stateISO`); - return { - value: stateISO, - keyForList: stateISO, - text: stateName, - isSelected: currentState === stateISO, - searchValue: StringUtils.sanitizeString(`${stateISO}${stateName}`), - }; - }), - [translate, currentState], - ); - - const searchResults = searchCountryOptions(searchValue, countryStates); - const headerMessage = searchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : ''; - - return ( - - - - - - - ); -} - -StateSelectorModal.propTypes = propTypes; -StateSelectorModal.defaultProps = defaultProps; -StateSelectorModal.displayName = 'StateSelectorModal'; - -export default StateSelectorModal; diff --git a/src/components/StatePicker/index.js b/src/components/StatePicker/index.js deleted file mode 100644 index f7f894af2a07..000000000000 --- a/src/components/StatePicker/index.js +++ /dev/null @@ -1,97 +0,0 @@ -import React, {useState} from 'react'; -import {View} from 'react-native'; -import PropTypes from 'prop-types'; -import _ from 'underscore'; -import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; -import styles from '../../styles/styles'; -import MenuItemWithTopDescription from '../MenuItemWithTopDescription'; -import useLocalize from '../../hooks/useLocalize'; -import FormHelpMessage from '../FormHelpMessage'; -import StateSelectorModal from './StateSelectorModal'; -import refPropTypes from '../refPropTypes'; - -const propTypes = { - /** Error text to display */ - errorText: PropTypes.string, - - /** State to display */ - value: PropTypes.string, - - /** Callback to call when the input changes */ - onInputChange: PropTypes.func, - - /** A ref to forward to MenuItemWithTopDescription */ - forwardedRef: refPropTypes, - - /** Label to display on field */ - label: PropTypes.string, -}; - -const defaultProps = { - value: undefined, - forwardedRef: undefined, - errorText: '', - onInputChange: () => {}, - label: undefined, -}; - -function StatePicker({value, errorText, onInputChange, forwardedRef, label}) { - const {translate} = useLocalize(); - const [isPickerVisible, setIsPickerVisible] = useState(false); - const [searchValue, setSearchValue] = useState(''); - - const showPickerModal = () => { - setIsPickerVisible(true); - }; - - const hidePickerModal = () => { - setIsPickerVisible(false); - }; - - const updateStateInput = (state) => { - if (state.value !== value) { - onInputChange(state.value); - } - hidePickerModal(); - }; - - const title = value && _.keys(COMMON_CONST.STATES).includes(value) ? translate(`allStates.${value}.stateName`) : ''; - const descStyle = title.length === 0 ? styles.textNormal : null; - - return ( - - - - - - - - ); -} - -StatePicker.propTypes = propTypes; -StatePicker.defaultProps = defaultProps; -StatePicker.displayName = 'StatePicker'; - -export default React.forwardRef((props, ref) => ( - -)); diff --git a/src/components/StateSelector.js b/src/components/StateSelector.js new file mode 100644 index 000000000000..c004fb8bf375 --- /dev/null +++ b/src/components/StateSelector.js @@ -0,0 +1,87 @@ +import React, {useEffect} from 'react'; +import PropTypes from 'prop-types'; +import {View} from 'react-native'; +import _ from 'underscore'; +import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; +import styles from '../styles/styles'; +import Navigation from '../libs/Navigation/Navigation'; +import ROUTES from '../ROUTES'; +import useLocalize from '../hooks/useLocalize'; +import MenuItemWithTopDescription from './MenuItemWithTopDescription'; +import FormHelpMessage from './FormHelpMessage'; + +const propTypes = { + /** Form error text. e.g when no country is selected */ + errorText: PropTypes.string, + + /** Callback called when the country changes. */ + onInputChange: PropTypes.func.isRequired, + + /** Current selected country */ + value: PropTypes.string, + + /** inputID used by the Form component */ + // eslint-disable-next-line react/no-unused-prop-types + inputID: PropTypes.string.isRequired, + + /** React ref being forwarded to the MenuItemWithTopDescription */ + forwardedRef: PropTypes.func, + + /** Label of state in the url */ + paramName: PropTypes.string, + + /** Label to display on field */ + label: PropTypes.string, +}; + +const defaultProps = { + errorText: '', + value: undefined, + forwardedRef: () => {}, + label: undefined, + paramName: 'state', +}; + +function StateSelector({errorText, value: stateCode, label, paramName, onInputChange, forwardedRef}) { + const {translate} = useLocalize(); + + const title = stateCode && _.keys(COMMON_CONST.STATES).includes(stateCode) ? translate(`allStates.${stateCode}.stateName`) : ''; + const descStyle = title.length === 0 ? styles.textNormal : null; + + useEffect(() => { + // This will cause the form to revalidate and remove any error related to country name + onInputChange(stateCode); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [stateCode]); + + return ( + + { + const activeRoute = Navigation.getActiveRoute(); + Navigation.navigate(ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS_STATE.getRoute(stateCode, activeRoute, label, paramName)); + }} + /> + + + + + ); +} + +StateSelector.propTypes = propTypes; +StateSelector.defaultProps = defaultProps; +StateSelector.displayName = 'StateSelector'; + +export default React.forwardRef((props, ref) => ( + +)); diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 6636702592c0..f682d438f270 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -127,6 +127,7 @@ const SettingsModalStackNavigator = createModalStackNavigator({ Settings_PersonalDetails_DateOfBirth: () => require('../../../pages/settings/Profile/PersonalDetails/DateOfBirthPage').default, Settings_PersonalDetails_Address: () => require('../../../pages/settings/Profile/PersonalDetails/AddressPage').default, Settings_PersonalDetails_Address_Country: () => require('../../../pages/settings/Profile/PersonalDetails/CountrySelectionPage').default, + Settings_PersonalDetails_Address_State: () => require('../../../pages/settings/Profile/PersonalDetails/StateSelectionPage').default, Settings_ContactMethods: () => require('../../../pages/settings/Profile/Contacts/ContactMethodsPage').default, Settings_ContactMethodDetails: () => require('../../../pages/settings/Profile/Contacts/ContactMethodDetailsPage').default, Settings_NewContactMethod: () => require('../../../pages/settings/Profile/Contacts/NewContactMethodPage').default, diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index bf069aba314e..1b39987e7257 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -155,6 +155,10 @@ export default { path: ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS_COUNTRY.route, exact: true, }, + Settings_PersonalDetails_Address_State: { + path: ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS_STATE.route, + exact: true, + }, Settings_TwoFactorAuth: { path: ROUTES.SETTINGS_2FA, exact: true, diff --git a/src/libs/getStateFromRoute.ts b/src/libs/getStateFromRoute.ts new file mode 100644 index 000000000000..ff01068eae3e --- /dev/null +++ b/src/libs/getStateFromRoute.ts @@ -0,0 +1,13 @@ +// eslint-disable-next-line you-dont-need-lodash-underscore/get +import lodashGet from 'lodash/get'; +import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; + +type RouteProps = { + params?: Record; +}; + +export default function getStateFromRoute(route: RouteProps, stateParamName = 'state'): string { + const stateFromUrlTemp = lodashGet(route, `params.${stateParamName}`) as unknown as string; + // check if state is valid + return lodashGet(COMMON_CONST.STATES, stateFromUrlTemp) ? stateFromUrlTemp : ''; +} diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js index d8fbc0290136..09e365b58c63 100644 --- a/src/pages/ReimbursementAccount/AddressForm.js +++ b/src/pages/ReimbursementAccount/AddressForm.js @@ -5,7 +5,7 @@ import TextInput from '../../components/TextInput'; import AddressSearch from '../../components/AddressSearch'; import styles from '../../styles/styles'; import CONST from '../../CONST'; -import StatePicker from '../../components/StatePicker'; +import StateSelector from '../../components/StateSelector'; const propTypes = { /** Translate key for Street name */ @@ -123,7 +123,7 @@ function AddressForm(props) { /> - - lodashGet(privatePersonalDetails, 'address') || {}, [privatePersonalDetails]); - const countryFromUrl = lodashGet(route, 'params.country'); + + const countryFromUrlTemp = lodashGet(route, 'params.country'); + // check if country is valid + const countryFromUrl = lodashGet(CONST.ALL_COUNTRIES, countryFromUrlTemp) ? countryFromUrlTemp : ''; + + const stateFromUrl = getStateFromRoute(route); const [currentCountry, setCurrentCountry] = useState(address.country); const zipSampleFormat = lodashGet(CONST.COUNTRY_ZIP_REGEX_DATA, [currentCountry, 'samples'], ''); const zipFormat = translate('common.zipCodeExampleFormat', {zipSampleFormat}); @@ -160,6 +166,13 @@ function AddressPage({privatePersonalDetails, route}) { handleAddressChange(countryFromUrl, 'country'); }, [countryFromUrl, handleAddressChange, currentCountry]); + useEffect(() => { + if (!stateFromUrl || stateFromUrl === state) { + return; + } + handleAddressChange(stateFromUrl, 'state'); + }, [state, handleAddressChange, stateFromUrl]); + return ( {isUSAForm ? ( - ) : ( diff --git a/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.js b/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.js new file mode 100644 index 000000000000..34364421b4ac --- /dev/null +++ b/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.js @@ -0,0 +1,137 @@ +import React, {useState, useMemo, useCallback} from 'react'; +import PropTypes from 'prop-types'; +import _ from 'underscore'; +import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; +import lodashGet from 'lodash/get'; +import Navigation from '../../../../libs/Navigation/Navigation'; +import ScreenWrapper from '../../../../components/ScreenWrapper'; +import HeaderWithBackButton from '../../../../components/HeaderWithBackButton'; +import SelectionList from '../../../../components/SelectionList'; +import searchCountryOptions from '../../../../libs/searchCountryOptions'; +import StringUtils from '../../../../libs/StringUtils'; +import useLocalize from '../../../../hooks/useLocalize'; +import styles from '../../../../styles/styles'; + +const propTypes = { + /** Route from navigation */ + route: PropTypes.shape({ + /** Params from the route */ + params: PropTypes.shape({ + /** Currently selected country */ + country: PropTypes.string, + + /** Route to navigate back after selecting a currency */ + backTo: PropTypes.string, + }), + }).isRequired, + + /** Navigation from react-navigation */ + navigation: PropTypes.shape({ + /** getState function retrieves the current navigation state from react-navigation's navigation property */ + getState: PropTypes.func.isRequired, + }).isRequired, +}; + +/** + * Appends or updates a query parameter in a given URL. + * + * @param {string} url - The original URL. + * @param {string} paramName - The name of the query parameter to append or update. + * @param {string} paramValue - The value of the query parameter to append or update. + * @returns {string} The updated URL with the appended or updated query parameter. + */ +function appendParam(url, paramName, paramValue) { + if (url.includes(`${paramName}=`)) { + // If parameter exists, replace it + const regex = new RegExp(`${paramName}=([^&]*)`); + return url.replace(regex, `${paramName}=${paramValue}`); + } + // If parameter doesn't exist, append it + const separator = url.includes('?') ? '&' : '?'; + return `${url}${separator}${paramName}=${paramValue}`; +} + +function StateSelectionPage({route, navigation}) { + const [searchValue, setSearchValue] = useState(''); + const {translate} = useLocalize(); + const currentState = lodashGet(route, 'params.state'); + const stateParamName = lodashGet(route, 'params.stateParamName') || 'state'; + const label = lodashGet(route, 'params.label'); + + const countryStates = useMemo( + () => + _.map(_.keys(COMMON_CONST.STATES), (state) => { + const stateName = translate(`allStates.${state}.stateName`); + const stateISO = translate(`allStates.${state}.stateISO`); + return { + value: stateISO, + keyForList: stateISO, + text: stateName, + isSelected: currentState === stateISO, + searchValue: StringUtils.sanitizeString(`${stateISO}${stateName}`), + }; + }), + [translate, currentState], + ); + + const searchResults = searchCountryOptions(searchValue, countryStates); + const headerMessage = searchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : ''; + + const selectCountryState = useCallback( + (option) => { + const backTo = lodashGet(route, 'params.backTo', ''); + + // Check the navigation state and "backTo" parameter to decide navigation behavior + if (navigation.getState().routes.length === 1 && _.isEmpty(backTo)) { + // If there is only one route and "backTo" is empty, go back in navigation + Navigation.goBack(); + } else if (!_.isEmpty(backTo) && navigation.getState().routes.length === 1) { + // If "backTo" is not empty and there is only one route, go back to the specific route defined in "backTo" with a country parameter + Navigation.goBack(appendParam(backTo, stateParamName, option.value)); + } else { + // Otherwise, navigate to the specific route defined in "backTo" with a country parameter + Navigation.navigate(appendParam(backTo, stateParamName, option.value)); + } + }, + [route, navigation, stateParamName], + ); + + return ( + + { + const backTo = lodashGet(route, 'params.backTo', ''); + let backToRoute = ''; + + if (backTo) { + backToRoute = appendParam(backTo, stateParamName, currentState); + } + + Navigation.goBack(backToRoute); + }} + /> + + + + ); +} + +StateSelectionPage.displayName = 'StateSelectionPage'; +StateSelectionPage.propTypes = propTypes; + +export default StateSelectionPage; diff --git a/src/pages/settings/Wallet/AddDebitCardPage.js b/src/pages/settings/Wallet/AddDebitCardPage.js index e75c3b2c517e..201a4e763693 100644 --- a/src/pages/settings/Wallet/AddDebitCardPage.js +++ b/src/pages/settings/Wallet/AddDebitCardPage.js @@ -11,7 +11,6 @@ import useLocalize from '../../../hooks/useLocalize'; import * as PaymentMethods from '../../../libs/actions/PaymentMethods'; import * as ValidationUtils from '../../../libs/ValidationUtils'; import CheckboxWithLabel from '../../../components/CheckboxWithLabel'; -import StatePicker from '../../../components/StatePicker'; import TextInput from '../../../components/TextInput'; import CONST from '../../../CONST'; import ONYXKEYS from '../../../ONYXKEYS'; @@ -22,6 +21,8 @@ import ROUTES from '../../../ROUTES'; import usePrevious from '../../../hooks/usePrevious'; import NotFoundPage from '../../ErrorPage/NotFoundPage'; import Permissions from '../../../libs/Permissions'; +import StateSelector from '../../../components/StateSelector'; +import getStateFromRoute from '../../../libs/getStateFromRoute'; const propTypes = { /* Onyx Props */ @@ -31,6 +32,15 @@ const propTypes = { /** List of betas available to current user */ betas: PropTypes.arrayOf(PropTypes.string), + + /** Route from navigation */ + route: PropTypes.shape({ + /** Params from the route */ + params: PropTypes.shape({ + /** Currently selected country */ + country: PropTypes.string, + }), + }).isRequired, }; const defaultProps = { @@ -104,6 +114,8 @@ function DebitCardPage(props) { return ; } + const stateFromUrl = getStateFromRoute(props.route); + return ( nameOnCardRef.current && nameOnCardRef.current.focus()} @@ -182,7 +194,10 @@ function DebitCardPage(props) { containerStyles={[styles.mt4]} /> - + From 1284e59cf00621f0012d38187e0738b1e7c3e8dc Mon Sep 17 00:00:00 2001 From: Yogesh Bhatt Date: Sat, 7 Oct 2023 14:43:44 +0530 Subject: [PATCH 0002/1082] Incorporation state resets bugfix --- src/pages/ReimbursementAccount/CompanyStep.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index ef7674894445..4bac76dbd164 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -262,7 +262,7 @@ function CompanyStep({reimbursementAccount, route, reimbursementAccountDraft, ge label={translate('companyStep.incorporationState')} shouldSaveDraft paramName="incorporationState" - value={incorporationState} + {...(incorporationState ? {value: incorporationState} : {})} /> Date: Sat, 7 Oct 2023 15:29:42 +0530 Subject: [PATCH 0003/1082] Modify pages to accomodate for StateSelection --- src/pages/EnablePayments/AdditionalDetailsStep.js | 15 ++++++++++++++- src/pages/EnablePayments/EnablePaymentsPage.js | 4 ++-- src/pages/ReimbursementAccount/ACHContractStep.js | 13 +++++++++++++ src/pages/ReimbursementAccount/CompanyStep.js | 4 ++-- .../ReimbursementAccountPage.js | 2 ++ src/pages/ReimbursementAccount/RequestorStep.js | 15 ++++++++++++++- .../Profile/PersonalDetails/AddressPage.js | 3 +++ .../Profile/PersonalDetails/StateSelectionPage.js | 4 ++-- src/pages/settings/Wallet/AddDebitCardPage.js | 4 ++-- 9 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js index c304103f69c7..f521346b5fcf 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.js +++ b/src/pages/EnablePayments/AdditionalDetailsStep.js @@ -24,6 +24,7 @@ import Form from '../../components/Form'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../components/withCurrentUserPersonalDetails'; import * as PersonalDetails from '../../libs/actions/PersonalDetails'; import OfflineIndicator from '../../components/OfflineIndicator'; +import getStateFromRoute from '../../libs/getStateFromRoute'; const propTypes = { ...withLocalizePropTypes, @@ -55,6 +56,15 @@ const propTypes = { /** Error code to determine additional behavior */ errorCode: PropTypes.string, }), + + /** Route from navigation */ + route: PropTypes.shape({ + /** Params from the route */ + params: PropTypes.shape({ + /** Currently selected state */ + state: PropTypes.string, + }), + }).isRequired, }; const defaultProps = { @@ -79,7 +89,7 @@ const fieldNameTranslationKeys = { ssnFull9: 'common.ssnFull9', }; -function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserPersonalDetails}) { +function AdditionalDetailsStep({walletAdditionalDetails, translate, route, currentUserPersonalDetails}) { const minDate = moment().subtract(CONST.DATE_BIRTH.MAX_AGE, 'Y').toDate(); const maxDate = moment().subtract(CONST.DATE_BIRTH.MIN_AGE_FOR_PAYMENT, 'Y').toDate(); const shouldAskForFullSSN = walletAdditionalDetails.errorCode === CONST.WALLET.ERROR.SSN; @@ -163,6 +173,8 @@ function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserP ); } + const state = getStateFromRoute(route); + return ( <> @@ -209,6 +221,7 @@ function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserP state: 'addressState', zipCode: 'addressZipCode', }} + values={state ? {state} : {}} translate={translate} streetTranslationKey={fieldNameTranslationKeys.addressStreet} shouldSaveDraft diff --git a/src/pages/EnablePayments/EnablePaymentsPage.js b/src/pages/EnablePayments/EnablePaymentsPage.js index f7ef2a174208..8feb8342ac8e 100644 --- a/src/pages/EnablePayments/EnablePaymentsPage.js +++ b/src/pages/EnablePayments/EnablePaymentsPage.js @@ -29,7 +29,7 @@ const defaultProps = { userWallet: {}, }; -function EnablePaymentsPage({userWallet}) { +function EnablePaymentsPage({userWallet, route}) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); @@ -72,7 +72,7 @@ function EnablePaymentsPage({userWallet}) { switch (currentStep) { case CONST.WALLET.STEP.ADDITIONAL_DETAILS: case CONST.WALLET.STEP.ADDITIONAL_DETAILS_KBA: - return ; + return ; case CONST.WALLET.STEP.ONFIDO: return ; case CONST.WALLET.STEP.TERMS: diff --git a/src/pages/ReimbursementAccount/ACHContractStep.js b/src/pages/ReimbursementAccount/ACHContractStep.js index 761be71d864a..7209d6250a94 100644 --- a/src/pages/ReimbursementAccount/ACHContractStep.js +++ b/src/pages/ReimbursementAccount/ACHContractStep.js @@ -19,12 +19,22 @@ import Form from '../../components/Form'; import * as FormActions from '../../libs/actions/FormActions'; import ScreenWrapper from '../../components/ScreenWrapper'; import StepPropTypes from './StepPropTypes'; +import getStateFromRoute from '../../libs/getStateFromRoute'; const propTypes = { ...StepPropTypes, /** Name of the company */ companyName: PropTypes.string.isRequired, + + /** Route from navigation */ + route: PropTypes.shape({ + /** Params from the route */ + params: PropTypes.shape({ + /** Currently selected state */ + state: PropTypes.string, + }), + }).isRequired, }; function ACHContractStep(props) { @@ -144,6 +154,8 @@ function ACHContractStep(props) { }); }; + const state = getStateFromRoute(props.route); + return ( { return errors; }; -function RequestorStep({reimbursementAccount, shouldShowOnfido, reimbursementAccountDraft, onBackButtonPress, getDefaultStateForField}) { +function RequestorStep({reimbursementAccount, route, shouldShowOnfido, reimbursementAccountDraft, onBackButtonPress, getDefaultStateForField}) { const {translate} = useLocalize(); const defaultValues = useMemo( @@ -118,6 +128,8 @@ function RequestorStep({reimbursementAccount, shouldShowOnfido, reimbursementAcc ); } + const state = getStateFromRoute(route); + return ( Date: Sun, 8 Oct 2023 08:24:10 +0530 Subject: [PATCH 0004/1082] Refactor to use useGeographicalStateFromRoute hook --- src/hooks/useGeographicalStateFromRoute.ts | 7 +++++++ src/libs/getStateFromRoute.ts | 3 ++- .../EnablePayments/AdditionalDetailsStep.js | 17 ++++------------- src/pages/EnablePayments/EnablePaymentsPage.js | 4 ++-- .../ReimbursementAccount/ACHContractStep.js | 13 ++----------- src/pages/ReimbursementAccount/CompanyStep.js | 18 +++++------------- .../ReimbursementAccountPage.js | 3 --- .../ReimbursementAccount/RequestorStep.js | 17 ++++------------- .../Profile/PersonalDetails/AddressPage.js | 7 ++----- src/pages/settings/Wallet/AddDebitCardPage.js | 15 +++------------ 10 files changed, 31 insertions(+), 73 deletions(-) create mode 100644 src/hooks/useGeographicalStateFromRoute.ts diff --git a/src/hooks/useGeographicalStateFromRoute.ts b/src/hooks/useGeographicalStateFromRoute.ts new file mode 100644 index 000000000000..33866e8b0cf1 --- /dev/null +++ b/src/hooks/useGeographicalStateFromRoute.ts @@ -0,0 +1,7 @@ +import {useRoute} from '@react-navigation/native'; +import getStateFromRoute from '../libs/getStateFromRoute'; + +export default function useGeographicalStateFromRoute(stateParamName = 'state') { + const route = useRoute(); + return getStateFromRoute(route, stateParamName); +} diff --git a/src/libs/getStateFromRoute.ts b/src/libs/getStateFromRoute.ts index ff01068eae3e..19d6de46014f 100644 --- a/src/libs/getStateFromRoute.ts +++ b/src/libs/getStateFromRoute.ts @@ -3,7 +3,8 @@ import lodashGet from 'lodash/get'; import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; type RouteProps = { - params?: Record; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + params?: Record; }; export default function getStateFromRoute(route: RouteProps, stateParamName = 'state'): string { diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js index 1d8691333069..5ab59899b139 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.js +++ b/src/pages/EnablePayments/AdditionalDetailsStep.js @@ -24,7 +24,7 @@ import Form from '../../components/Form'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../components/withCurrentUserPersonalDetails'; import * as PersonalDetails from '../../libs/actions/PersonalDetails'; import OfflineIndicator from '../../components/OfflineIndicator'; -import getStateFromRoute from '../../libs/getStateFromRoute'; +import useGeographicalStateFromRoute from '../../hooks/useGeographicalStateFromRoute'; const propTypes = { ...withLocalizePropTypes, @@ -56,15 +56,6 @@ const propTypes = { /** Error code to determine additional behavior */ errorCode: PropTypes.string, }), - - /** Route from navigation */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** Currently selected state */ - state: PropTypes.string, - }), - }).isRequired, }; const defaultProps = { @@ -89,7 +80,7 @@ const fieldNameTranslationKeys = { ssnFull9: 'common.ssnFull9', }; -function AdditionalDetailsStep({walletAdditionalDetails, route, translate, currentUserPersonalDetails}) { +function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserPersonalDetails}) { const currentDate = new Date(); const minDate = subYears(currentDate, CONST.DATE_BIRTH.MAX_AGE); const maxDate = subYears(currentDate, CONST.DATE_BIRTH.MIN_AGE_FOR_PAYMENT); @@ -155,6 +146,8 @@ function AdditionalDetailsStep({walletAdditionalDetails, route, translate, curre Wallet.updatePersonalDetails(personalDetails); }; + const state = useGeographicalStateFromRoute(); + if (!_.isEmpty(walletAdditionalDetails.questions)) { return ( diff --git a/src/pages/EnablePayments/EnablePaymentsPage.js b/src/pages/EnablePayments/EnablePaymentsPage.js index 8feb8342ac8e..f7ef2a174208 100644 --- a/src/pages/EnablePayments/EnablePaymentsPage.js +++ b/src/pages/EnablePayments/EnablePaymentsPage.js @@ -29,7 +29,7 @@ const defaultProps = { userWallet: {}, }; -function EnablePaymentsPage({userWallet, route}) { +function EnablePaymentsPage({userWallet}) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); @@ -72,7 +72,7 @@ function EnablePaymentsPage({userWallet, route}) { switch (currentStep) { case CONST.WALLET.STEP.ADDITIONAL_DETAILS: case CONST.WALLET.STEP.ADDITIONAL_DETAILS_KBA: - return ; + return ; case CONST.WALLET.STEP.ONFIDO: return ; case CONST.WALLET.STEP.TERMS: diff --git a/src/pages/ReimbursementAccount/ACHContractStep.js b/src/pages/ReimbursementAccount/ACHContractStep.js index 7209d6250a94..b619abd68a33 100644 --- a/src/pages/ReimbursementAccount/ACHContractStep.js +++ b/src/pages/ReimbursementAccount/ACHContractStep.js @@ -19,22 +19,13 @@ import Form from '../../components/Form'; import * as FormActions from '../../libs/actions/FormActions'; import ScreenWrapper from '../../components/ScreenWrapper'; import StepPropTypes from './StepPropTypes'; -import getStateFromRoute from '../../libs/getStateFromRoute'; +import useGeographicalStateFromRoute from '../../hooks/useGeographicalStateFromRoute'; const propTypes = { ...StepPropTypes, /** Name of the company */ companyName: PropTypes.string.isRequired, - - /** Route from navigation */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** Currently selected state */ - state: PropTypes.string, - }), - }).isRequired, }; function ACHContractStep(props) { @@ -154,7 +145,7 @@ function ACHContractStep(props) { }); }; - const state = getStateFromRoute(props.route); + const state = useGeographicalStateFromRoute(); return ( diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js index 2dc71f7b80fa..c6f53c2474f4 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js @@ -429,7 +429,6 @@ class ReimbursementAccountPage extends React.Component { if (currentStep === CONST.BANK_ACCOUNT.STEP.COMPANY) { return ( { return errors; }; -function RequestorStep({reimbursementAccount, route, shouldShowOnfido, reimbursementAccountDraft, onBackButtonPress, getDefaultStateForField}) { +function RequestorStep({reimbursementAccount, shouldShowOnfido, reimbursementAccountDraft, onBackButtonPress, getDefaultStateForField}) { const {translate} = useLocalize(); const defaultValues = useMemo( @@ -117,6 +108,8 @@ function RequestorStep({reimbursementAccount, route, shouldShowOnfido, reimburse ); + const state = useGeographicalStateFromRoute(); + if (shouldShowOnfido) { return ( ; } - const stateFromUrl = getStateFromRoute(props.route); - return ( nameOnCardRef.current && nameOnCardRef.current.focus()} From 0f83cff67014fc53ffa0d005247a0cc29f1a8a3f Mon Sep 17 00:00:00 2001 From: Yogesh Bhatt Date: Sat, 28 Oct 2023 02:35:21 +0530 Subject: [PATCH 0005/1082] move getStateFromRoute to useGeographicalStateFromRoute --- src/hooks/useGeographicalStateFromRoute.ts | 8 ++++++-- src/libs/getStateFromRoute.ts | 14 -------------- 2 files changed, 6 insertions(+), 16 deletions(-) delete mode 100644 src/libs/getStateFromRoute.ts diff --git a/src/hooks/useGeographicalStateFromRoute.ts b/src/hooks/useGeographicalStateFromRoute.ts index 33866e8b0cf1..3726499edf8f 100644 --- a/src/hooks/useGeographicalStateFromRoute.ts +++ b/src/hooks/useGeographicalStateFromRoute.ts @@ -1,7 +1,11 @@ import {useRoute} from '@react-navigation/native'; -import getStateFromRoute from '../libs/getStateFromRoute'; +// eslint-disable-next-line you-dont-need-lodash-underscore/get +import lodashGet from 'lodash/get'; +import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; export default function useGeographicalStateFromRoute(stateParamName = 'state') { const route = useRoute(); - return getStateFromRoute(route, stateParamName); + const stateFromUrlTemp = lodashGet(route, `params.${stateParamName}`) as unknown as string; + // check if state is valid + return lodashGet(COMMON_CONST.STATES, stateFromUrlTemp) ? stateFromUrlTemp : ''; } diff --git a/src/libs/getStateFromRoute.ts b/src/libs/getStateFromRoute.ts deleted file mode 100644 index 19d6de46014f..000000000000 --- a/src/libs/getStateFromRoute.ts +++ /dev/null @@ -1,14 +0,0 @@ -// eslint-disable-next-line you-dont-need-lodash-underscore/get -import lodashGet from 'lodash/get'; -import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; - -type RouteProps = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - params?: Record; -}; - -export default function getStateFromRoute(route: RouteProps, stateParamName = 'state'): string { - const stateFromUrlTemp = lodashGet(route, `params.${stateParamName}`) as unknown as string; - // check if state is valid - return lodashGet(COMMON_CONST.STATES, stateFromUrlTemp) ? stateFromUrlTemp : ''; -} From 0951d5d75b56a38009fe7bbd91d576bbd976cb25 Mon Sep 17 00:00:00 2001 From: Yogesh Bhatt Date: Sat, 28 Oct 2023 02:46:35 +0530 Subject: [PATCH 0006/1082] Add displayname to StateSelectorWithRef --- src/components/StateSelector.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/StateSelector.js b/src/components/StateSelector.js index c004fb8bf375..ba7705802982 100644 --- a/src/components/StateSelector.js +++ b/src/components/StateSelector.js @@ -78,10 +78,14 @@ StateSelector.propTypes = propTypes; StateSelector.defaultProps = defaultProps; StateSelector.displayName = 'StateSelector'; -export default React.forwardRef((props, ref) => ( +const StateSelectorWithRef = React.forwardRef((props, ref) => ( )); + +StateSelectorWithRef.displayName = 'StateSelectorWithRef'; + +export default StateSelectorWithRef; From 598c7c6c3378fe402e3b811a83f70b4cf16c38de Mon Sep 17 00:00:00 2001 From: Yogesh Bhatt Date: Sat, 28 Oct 2023 03:57:15 +0530 Subject: [PATCH 0007/1082] Fix AddressPage: State not changing on autocomplete --- src/pages/settings/Profile/PersonalDetails/AddressPage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Profile/PersonalDetails/AddressPage.js b/src/pages/settings/Profile/PersonalDetails/AddressPage.js index 1eec60d7d57f..112372abebcd 100644 --- a/src/pages/settings/Profile/PersonalDetails/AddressPage.js +++ b/src/pages/settings/Profile/PersonalDetails/AddressPage.js @@ -167,11 +167,11 @@ function AddressPage({privatePersonalDetails, route}) { }, [countryFromUrl, handleAddressChange]); useEffect(() => { - if (!stateFromUrl || stateFromUrl === state) { + if (!stateFromUrl) { return; } handleAddressChange(stateFromUrl, 'state'); - }, [state, handleAddressChange, stateFromUrl]); + }, [handleAddressChange, stateFromUrl]); return ( Date: Sat, 28 Oct 2023 04:56:23 +0530 Subject: [PATCH 0008/1082] Refactor StateSelector logic --- src/components/StateSelector.js | 30 +++++++++++++++---- .../EnablePayments/AdditionalDetailsStep.js | 4 --- .../ReimbursementAccount/ACHContractStep.js | 4 --- src/pages/ReimbursementAccount/CompanyStep.js | 3 +- .../ReimbursementAccount/RequestorStep.js | 4 --- src/pages/settings/Wallet/AddDebitCardPage.js | 8 +---- 6 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/components/StateSelector.js b/src/components/StateSelector.js index ba7705802982..52df1f61d7e4 100644 --- a/src/components/StateSelector.js +++ b/src/components/StateSelector.js @@ -1,4 +1,4 @@ -import React, {useEffect} from 'react'; +import React, {useEffect, useState} from 'react'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import _ from 'underscore'; @@ -9,6 +9,7 @@ import ROUTES from '../ROUTES'; import useLocalize from '../hooks/useLocalize'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import FormHelpMessage from './FormHelpMessage'; +import useGeographicalStateFromRoute from '../hooks/useGeographicalStateFromRoute'; const propTypes = { /** Form error text. e.g when no country is selected */ @@ -32,6 +33,9 @@ const propTypes = { /** Label to display on field */ label: PropTypes.string, + + /** whether to use state from url */ + useStateFromUrl: PropTypes.bool, }; const defaultProps = { @@ -40,20 +44,36 @@ const defaultProps = { forwardedRef: () => {}, label: undefined, paramName: 'state', + useStateFromUrl: true, }; -function StateSelector({errorText, value: stateCode, label, paramName, onInputChange, forwardedRef}) { +function StateSelector({errorText, useStateFromUrl, value: stateCode, label, paramName, onInputChange, forwardedRef}) { const {translate} = useLocalize(); - const title = stateCode && _.keys(COMMON_CONST.STATES).includes(stateCode) ? translate(`allStates.${stateCode}.stateName`) : ''; - const descStyle = title.length === 0 ? styles.textNormal : null; + const stateFromUrl = useGeographicalStateFromRoute(paramName); + + const [stateToDisplay, setStateToDisplay] = useState(''); + + useEffect(() => { + if (useStateFromUrl) { + // This will cause the form to revalidate and remove any error related to country name + stateFromUrl && onInputChange(stateFromUrl); + stateFromUrl && setStateToDisplay(stateFromUrl); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [stateFromUrl, useStateFromUrl]); useEffect(() => { // This will cause the form to revalidate and remove any error related to country name - onInputChange(stateCode); + stateCode && onInputChange(stateCode); + stateCode && setStateToDisplay(stateCode); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [stateCode]); + const title = stateToDisplay && _.keys(COMMON_CONST.STATES).includes(stateToDisplay) ? translate(`allStates.${stateToDisplay}.stateName`) : ''; + const descStyle = title.length === 0 ? styles.textNormal : null; + return ( diff --git a/src/pages/ReimbursementAccount/RequestorStep.js b/src/pages/ReimbursementAccount/RequestorStep.js index 875c884420ae..86f1d5077eca 100644 --- a/src/pages/ReimbursementAccount/RequestorStep.js +++ b/src/pages/ReimbursementAccount/RequestorStep.js @@ -17,7 +17,6 @@ import Form from '../../components/Form'; import ScreenWrapper from '../../components/ScreenWrapper'; import useLocalize from '../../hooks/useLocalize'; import {reimbursementAccountPropTypes} from './reimbursementAccountPropTypes'; -import useGeographicalStateFromRoute from '../../hooks/useGeographicalStateFromRoute'; const propTypes = { onBackButtonPress: PropTypes.func.isRequired, @@ -106,8 +105,6 @@ function RequestorStep({reimbursementAccount, shouldShowOnfido, onBackButtonPres ); - const state = useGeographicalStateFromRoute(); - if (shouldShowOnfido) { return ( ; } @@ -185,10 +182,7 @@ function DebitCardPage(props) { containerStyles={[styles.mt4]} /> - + Date: Sat, 28 Oct 2023 05:02:05 +0530 Subject: [PATCH 0009/1082] Refactor AddressPage --- src/pages/settings/Profile/PersonalDetails/AddressPage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/settings/Profile/PersonalDetails/AddressPage.js b/src/pages/settings/Profile/PersonalDetails/AddressPage.js index 112372abebcd..1e21a210dd40 100644 --- a/src/pages/settings/Profile/PersonalDetails/AddressPage.js +++ b/src/pages/settings/Profile/PersonalDetails/AddressPage.js @@ -232,6 +232,7 @@ function AddressPage({privatePersonalDetails, route}) { {isUSAForm ? ( From 8a293b5ceeaec50be8bcd7d262a097b46b0b783f Mon Sep 17 00:00:00 2001 From: Yogesh Bhatt Date: Sat, 28 Oct 2023 05:34:30 +0530 Subject: [PATCH 0010/1082] linting fixes StateSelector --- src/components/StateSelector.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/StateSelector.js b/src/components/StateSelector.js index 52df1f61d7e4..517142527552 100644 --- a/src/components/StateSelector.js +++ b/src/components/StateSelector.js @@ -55,18 +55,21 @@ function StateSelector({errorText, useStateFromUrl, value: stateCode, label, par const [stateToDisplay, setStateToDisplay] = useState(''); useEffect(() => { - if (useStateFromUrl) { - // This will cause the form to revalidate and remove any error related to country name - stateFromUrl && onInputChange(stateFromUrl); - stateFromUrl && setStateToDisplay(stateFromUrl); - } + if (!useStateFromUrl || !stateFromUrl) return; + + // This will cause the form to revalidate and remove any error related to country name + onInputChange(stateFromUrl); + setStateToDisplay(stateFromUrl); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [stateFromUrl, useStateFromUrl]); useEffect(() => { + if (!stateCode) return; + // This will cause the form to revalidate and remove any error related to country name - stateCode && onInputChange(stateCode); - stateCode && setStateToDisplay(stateCode); + onInputChange(stateCode); + setStateToDisplay(stateCode); // eslint-disable-next-line react-hooks/exhaustive-deps }, [stateCode]); From c946cbb839e04468d5c2acedaac81905bb9233a9 Mon Sep 17 00:00:00 2001 From: Yogesh Bhatt Date: Sat, 28 Oct 2023 09:07:58 +0530 Subject: [PATCH 0011/1082] linting --- src/components/StateSelector.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/StateSelector.js b/src/components/StateSelector.js index 517142527552..aaa905d8940f 100644 --- a/src/components/StateSelector.js +++ b/src/components/StateSelector.js @@ -55,7 +55,9 @@ function StateSelector({errorText, useStateFromUrl, value: stateCode, label, par const [stateToDisplay, setStateToDisplay] = useState(''); useEffect(() => { - if (!useStateFromUrl || !stateFromUrl) return; + if (!useStateFromUrl || !stateFromUrl) { + return; + } // This will cause the form to revalidate and remove any error related to country name onInputChange(stateFromUrl); @@ -65,7 +67,9 @@ function StateSelector({errorText, useStateFromUrl, value: stateCode, label, par }, [stateFromUrl, useStateFromUrl]); useEffect(() => { - if (!stateCode) return; + if (!stateCode) { + return; + } // This will cause the form to revalidate and remove any error related to country name onInputChange(stateCode); From 144eea9e55e6bc9190ab71da057a0d20e0b04e39 Mon Sep 17 00:00:00 2001 From: Yogesh Bhatt Date: Sun, 29 Oct 2023 10:28:58 +0530 Subject: [PATCH 0012/1082] StateSelector and related files refactoring --- src/components/StateSelector.js | 2 +- src/pages/ReimbursementAccount/CompanyStep.js | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/components/StateSelector.js b/src/components/StateSelector.js index aaa905d8940f..c1ebdaefc493 100644 --- a/src/components/StateSelector.js +++ b/src/components/StateSelector.js @@ -34,7 +34,7 @@ const propTypes = { /** Label to display on field */ label: PropTypes.string, - /** whether to use state from url */ + /** whether to use state from url, for cases when url value is passed from parent */ useStateFromUrl: PropTypes.bool, }; diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 7e86ebf139b2..03a222abe3d7 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -25,7 +25,6 @@ import Form from '../../components/Form'; import ScreenWrapper from '../../components/ScreenWrapper'; import StepPropTypes from './StepPropTypes'; import StateSelector from '../../components/StateSelector'; -import useGeographicalStateFromRoute from '../../hooks/useGeographicalStateFromRoute'; const propTypes = { ...StepPropTypes, @@ -140,8 +139,6 @@ function CompanyStep({reimbursementAccount, reimbursementAccountDraft, getDefaul const shouldDisableCompanyName = Boolean(bankAccountID && getDefaultStateForField('companyName')); const shouldDisableCompanyTaxID = Boolean(bankAccountID && getDefaultStateForField('companyTaxID')); - const incorporationState = useGeographicalStateFromRoute('incorporationState') || getDefaultStateForField('incorporationState'); - return ( Date: Sun, 29 Oct 2023 11:18:33 +0530 Subject: [PATCH 0013/1082] Fix incorrect merge --- .../Profile/PersonalDetails/AddressPage.js | 7 +------ .../PersonalDetails/StateSelectionPage.js | 16 ++++++++-------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/pages/settings/Profile/PersonalDetails/AddressPage.js b/src/pages/settings/Profile/PersonalDetails/AddressPage.js index d3b9bdfba141..9c102e566b06 100644 --- a/src/pages/settings/Profile/PersonalDetails/AddressPage.js +++ b/src/pages/settings/Profile/PersonalDetails/AddressPage.js @@ -4,12 +4,6 @@ import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import AddressSearch from '@components/AddressSearch'; import CountrySelector from '@components/CountrySelector'; @@ -28,6 +22,7 @@ import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import useGeographicalStateFromRoute from '@hooks/useGeographicalStateFromRoute'; const propTypes = { /* Onyx Props */ diff --git a/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.js b/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.js index ea443edc581d..37acb6e38397 100644 --- a/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.js +++ b/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.js @@ -3,14 +3,14 @@ import PropTypes from 'prop-types'; import _ from 'underscore'; import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; import lodashGet from 'lodash/get'; -import Navigation from '../../../../libs/Navigation/Navigation'; -import ScreenWrapper from '../../../../components/ScreenWrapper'; -import HeaderWithBackButton from '../../../../components/HeaderWithBackButton'; -import SelectionList from '../../../../components/SelectionList'; -import searchCountryOptions from '../../../../libs/searchCountryOptions'; -import StringUtils from '../../../../libs/StringUtils'; -import useLocalize from '../../../../hooks/useLocalize'; -import styles from '../../../../styles/styles'; +import Navigation from '@libs/Navigation/Navigation'; +import ScreenWrapper from '@components/ScreenWrapper'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import SelectionList from '@components/SelectionList'; +import searchCountryOptions from '@libs/searchCountryOptions'; +import StringUtils from '@libs/StringUtils'; +import useLocalize from '@hooks/useLocalize'; +import styles from '@styles/styles'; const propTypes = { /** Route from navigation */ From 9fb4f72463d13aca2f897cbc0e17a4aa399c4b6a Mon Sep 17 00:00:00 2001 From: Yogesh Bhatt Date: Sun, 29 Oct 2023 11:22:35 +0530 Subject: [PATCH 0014/1082] Reverted scripts/shellCheck.sh to state at 33f15e8d85b3366b1a09bca914f61d5caa4a2fb5 --- scripts/shellCheck.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/shellCheck.sh b/scripts/shellCheck.sh index 18206a419509..d148958900d4 100755 --- a/scripts/shellCheck.sh +++ b/scripts/shellCheck.sh @@ -45,4 +45,4 @@ if [ $EXIT_CODE == 0 ]; then success "ShellCheck passed for all files!" fi -exit $EXIT_CODE \ No newline at end of file +exit $EXIT_CODE From d6a643c7b7f4db06243c420606afae458fb9ade8 Mon Sep 17 00:00:00 2001 From: Yogesh Bhatt Date: Sun, 29 Oct 2023 11:27:55 +0530 Subject: [PATCH 0015/1082] Linting --- src/components/StateSelector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/StateSelector.js b/src/components/StateSelector.js index b5b1c3903c8c..c27c0e57e4c9 100644 --- a/src/components/StateSelector.js +++ b/src/components/StateSelector.js @@ -7,9 +7,9 @@ import styles from '@styles/styles'; import Navigation from '@libs/Navigation/Navigation'; import ROUTES from '@src/ROUTES'; import useLocalize from '@hooks/useLocalize'; +import useGeographicalStateFromRoute from '@hooks/useGeographicalStateFromRoute'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import FormHelpMessage from './FormHelpMessage'; -import useGeographicalStateFromRoute from '@hooks/useGeographicalStateFromRoute'; const propTypes = { /** Form error text. e.g when no country is selected */ From 9bb88ee2129ebebdc9274598a72ab69ba84ce3c4 Mon Sep 17 00:00:00 2001 From: Yogesh Bhatt <65986357+ygshbht@users.noreply.github.com> Date: Tue, 31 Oct 2023 21:29:36 +0530 Subject: [PATCH 0016/1082] Comment formatting src/hooks/useGeographicalStateFromRoute.ts Co-authored-by: Rajat Parashar --- src/hooks/useGeographicalStateFromRoute.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useGeographicalStateFromRoute.ts b/src/hooks/useGeographicalStateFromRoute.ts index 3726499edf8f..d86b9df6ce24 100644 --- a/src/hooks/useGeographicalStateFromRoute.ts +++ b/src/hooks/useGeographicalStateFromRoute.ts @@ -6,6 +6,6 @@ import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; export default function useGeographicalStateFromRoute(stateParamName = 'state') { const route = useRoute(); const stateFromUrlTemp = lodashGet(route, `params.${stateParamName}`) as unknown as string; - // check if state is valid + // Check if state is valid return lodashGet(COMMON_CONST.STATES, stateFromUrlTemp) ? stateFromUrlTemp : ''; } From 5e3844785f3ef11d3bdb411078d2482490aa35bc Mon Sep 17 00:00:00 2001 From: Yogesh Bhatt Date: Tue, 31 Oct 2023 22:00:09 +0530 Subject: [PATCH 0017/1082] Rename useStateFomrUrl to shouldUseStateFromUrl --- src/components/StateSelector.js | 10 +++++----- .../settings/Profile/PersonalDetails/AddressPage.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/StateSelector.js b/src/components/StateSelector.js index c27c0e57e4c9..fb4046ad848b 100644 --- a/src/components/StateSelector.js +++ b/src/components/StateSelector.js @@ -35,7 +35,7 @@ const propTypes = { label: PropTypes.string, /** whether to use state from url, for cases when url value is passed from parent */ - useStateFromUrl: PropTypes.bool, + shouldUseStateFromUrl: PropTypes.bool, }; const defaultProps = { @@ -44,10 +44,10 @@ const defaultProps = { forwardedRef: () => {}, label: undefined, paramName: 'state', - useStateFromUrl: true, + shouldUseStateFromUrl: true, }; -function StateSelector({errorText, useStateFromUrl, value: stateCode, label, paramName, onInputChange, forwardedRef}) { +function StateSelector({errorText, shouldUseStateFromUrl, value: stateCode, label, paramName, onInputChange, forwardedRef}) { const {translate} = useLocalize(); const stateFromUrl = useGeographicalStateFromRoute(paramName); @@ -55,7 +55,7 @@ function StateSelector({errorText, useStateFromUrl, value: stateCode, label, par const [stateToDisplay, setStateToDisplay] = useState(''); useEffect(() => { - if (!useStateFromUrl || !stateFromUrl) { + if (!shouldUseStateFromUrl || !stateFromUrl) { return; } @@ -64,7 +64,7 @@ function StateSelector({errorText, useStateFromUrl, value: stateCode, label, par setStateToDisplay(stateFromUrl); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [stateFromUrl, useStateFromUrl]); + }, [stateFromUrl, shouldUseStateFromUrl]); useEffect(() => { if (!stateCode) { diff --git a/src/pages/settings/Profile/PersonalDetails/AddressPage.js b/src/pages/settings/Profile/PersonalDetails/AddressPage.js index 9c102e566b06..a9b64b2420ed 100644 --- a/src/pages/settings/Profile/PersonalDetails/AddressPage.js +++ b/src/pages/settings/Profile/PersonalDetails/AddressPage.js @@ -232,7 +232,7 @@ function AddressPage({privatePersonalDetails, route}) { {isUSAForm ? ( From 470ec03d5e71e0580643a852a14fe2ee61e4f241 Mon Sep 17 00:00:00 2001 From: Yogesh Bhatt Date: Tue, 31 Oct 2023 22:07:00 +0530 Subject: [PATCH 0018/1082] forwardedRef type correction StateSelector --- src/components/StateSelector.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/StateSelector.js b/src/components/StateSelector.js index fb4046ad848b..59f974251143 100644 --- a/src/components/StateSelector.js +++ b/src/components/StateSelector.js @@ -10,6 +10,7 @@ import useLocalize from '@hooks/useLocalize'; import useGeographicalStateFromRoute from '@hooks/useGeographicalStateFromRoute'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import FormHelpMessage from './FormHelpMessage'; +import refPropTypes from './refPropTypes'; const propTypes = { /** Form error text. e.g when no country is selected */ @@ -26,7 +27,7 @@ const propTypes = { inputID: PropTypes.string.isRequired, /** React ref being forwarded to the MenuItemWithTopDescription */ - forwardedRef: PropTypes.func, + forwardedRef: refPropTypes, /** Label of state in the url */ paramName: PropTypes.string, From 8f28580d6dca027d9698a8510a0ebaf28a6ba38d Mon Sep 17 00:00:00 2001 From: Yogesh Bhatt Date: Tue, 31 Oct 2023 22:08:46 +0530 Subject: [PATCH 0019/1082] StateSelector: rename paramName to queryParam --- src/components/StateSelector.js | 10 +++++----- src/pages/ReimbursementAccount/CompanyStep.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/StateSelector.js b/src/components/StateSelector.js index 59f974251143..13202f9d79e1 100644 --- a/src/components/StateSelector.js +++ b/src/components/StateSelector.js @@ -30,7 +30,7 @@ const propTypes = { forwardedRef: refPropTypes, /** Label of state in the url */ - paramName: PropTypes.string, + queryParam: PropTypes.string, /** Label to display on field */ label: PropTypes.string, @@ -44,14 +44,14 @@ const defaultProps = { value: undefined, forwardedRef: () => {}, label: undefined, - paramName: 'state', + queryParam: 'state', shouldUseStateFromUrl: true, }; -function StateSelector({errorText, shouldUseStateFromUrl, value: stateCode, label, paramName, onInputChange, forwardedRef}) { +function StateSelector({errorText, shouldUseStateFromUrl, value: stateCode, label, queryParam, onInputChange, forwardedRef}) { const {translate} = useLocalize(); - const stateFromUrl = useGeographicalStateFromRoute(paramName); + const stateFromUrl = useGeographicalStateFromRoute(queryParam); const [stateToDisplay, setStateToDisplay] = useState(''); @@ -92,7 +92,7 @@ function StateSelector({errorText, shouldUseStateFromUrl, value: stateCode, labe description={label || translate('common.state')} onPress={() => { const activeRoute = Navigation.getActiveRoute(); - Navigation.navigate(ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS_STATE.getRoute(stateCode, activeRoute, label, paramName)); + Navigation.navigate(ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS_STATE.getRoute(stateCode, activeRoute, label, queryParam)); }} /> diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index fa806521e5d0..04010f1db0c1 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -248,7 +248,7 @@ function CompanyStep({reimbursementAccount, reimbursementAccountDraft, getDefaul label={translate('companyStep.incorporationState')} defaultValue={getDefaultStateForField('incorporationState')} shouldSaveDraft - paramName="incorporationState" + queryParam="incorporationState" /> Date: Wed, 1 Nov 2023 08:24:52 +0530 Subject: [PATCH 0020/1082] Remove lodashGet from useGeographicalStateFromRoute --- src/hooks/useGeographicalStateFromRoute.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/hooks/useGeographicalStateFromRoute.ts b/src/hooks/useGeographicalStateFromRoute.ts index d86b9df6ce24..112e0e8db69c 100644 --- a/src/hooks/useGeographicalStateFromRoute.ts +++ b/src/hooks/useGeographicalStateFromRoute.ts @@ -1,11 +1,14 @@ -import {useRoute} from '@react-navigation/native'; -// eslint-disable-next-line you-dont-need-lodash-underscore/get -import lodashGet from 'lodash/get'; +import {ParamListBase, RouteProp, useRoute} from '@react-navigation/native'; import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; +type CustomParamList = ParamListBase & Record>; + export default function useGeographicalStateFromRoute(stateParamName = 'state') { - const route = useRoute(); - const stateFromUrlTemp = lodashGet(route, `params.${stateParamName}`) as unknown as string; - // Check if state is valid - return lodashGet(COMMON_CONST.STATES, stateFromUrlTemp) ? stateFromUrlTemp : ''; + const route = useRoute>(); + const stateFromUrlTemp = route.params?.[stateParamName] as string | undefined; + + if (!stateFromUrlTemp) { + return ''; + } + return COMMON_CONST.STATES[stateFromUrlTemp as keyof typeof COMMON_CONST.STATES] ? stateFromUrlTemp : ''; } From 428e48c177adcf9a77f0067114781307be11ea62 Mon Sep 17 00:00:00 2001 From: Yogesh Bhatt Date: Wed, 1 Nov 2023 08:37:52 +0530 Subject: [PATCH 0021/1082] StateSelectionPage appendParam restructuring --- src/libs/Url.ts | 16 +++++++++- .../PersonalDetails/StateSelectionPage.js | 32 ++++--------------- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/libs/Url.ts b/src/libs/Url.ts index a21f007e8468..7e028aded60c 100644 --- a/src/libs/Url.ts +++ b/src/libs/Url.ts @@ -41,4 +41,18 @@ function hasSameExpensifyOrigin(url1: string, url2: string): boolean { } } -export {addTrailingForwardSlash, hasSameExpensifyOrigin, getPathFromURL}; +/** + * Appends or updates a query parameter in a given URL. + */ +function appendParam(url: string, paramName: string, paramValue: string) { + if (url.includes(`${paramName}=`)) { + // If parameter exists, replace it + const regex = new RegExp(`${paramName}=([^&]*)`); + return url.replace(regex, `${paramName}=${paramValue}`); + } + // If parameter doesn't exist, append it + const separator = url.includes('?') ? '&' : '?'; + return `${url}${separator}${paramName}=${paramValue}`; +} + +export {addTrailingForwardSlash, hasSameExpensifyOrigin, getPathFromURL, appendParam}; diff --git a/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.js b/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.js index 37acb6e38397..a506d7e61495 100644 --- a/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.js +++ b/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.js @@ -1,15 +1,16 @@ -import React, {useState, useMemo, useCallback} from 'react'; -import PropTypes from 'prop-types'; -import _ from 'underscore'; import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; import lodashGet from 'lodash/get'; -import Navigation from '@libs/Navigation/Navigation'; -import ScreenWrapper from '@components/ScreenWrapper'; +import PropTypes from 'prop-types'; +import React, {useCallback, useMemo, useState} from 'react'; +import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; +import useLocalize from '@hooks/useLocalize'; +import Navigation from '@libs/Navigation/Navigation'; import searchCountryOptions from '@libs/searchCountryOptions'; import StringUtils from '@libs/StringUtils'; -import useLocalize from '@hooks/useLocalize'; +import {appendParam} from '@libs/Url'; import styles from '@styles/styles'; const propTypes = { @@ -32,25 +33,6 @@ const propTypes = { }).isRequired, }; -/** - * Appends or updates a query parameter in a given URL. - * - * @param {string} url - The original URL. - * @param {string} paramName - The name of the query parameter to append or update. - * @param {string} paramValue - The value of the query parameter to append or update. - * @returns {string} The updated URL with the appended or updated query parameter. - */ -function appendParam(url, paramName, paramValue) { - if (url.includes(`${paramName}=`)) { - // If parameter exists, replace it - const regex = new RegExp(`${paramName}=([^&]*)`); - return url.replace(regex, `${paramName}=${paramValue}`); - } - // If parameter doesn't exist, append it - const separator = url.includes('?') ? '&' : '?'; - return `${url}${separator}${paramName}=${paramValue}`; -} - function StateSelectionPage({route, navigation}) { const [searchValue, setSearchValue] = useState(''); const {translate} = useLocalize(); From 7cd8b6fb2c30a45999f521355e97fdadfe6f4b2f Mon Sep 17 00:00:00 2001 From: Yogesh Bhatt Date: Wed, 1 Nov 2023 08:43:07 +0530 Subject: [PATCH 0022/1082] Prettier formatting --- src/components/StateSelector.js | 12 ++++++------ .../settings/Profile/PersonalDetails/AddressPage.js | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/StateSelector.js b/src/components/StateSelector.js index 13202f9d79e1..121037d91370 100644 --- a/src/components/StateSelector.js +++ b/src/components/StateSelector.js @@ -1,15 +1,15 @@ -import React, {useEffect, useState} from 'react'; +import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; import PropTypes from 'prop-types'; +import React, {useEffect, useState} from 'react'; import {View} from 'react-native'; import _ from 'underscore'; -import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; -import styles from '@styles/styles'; +import useGeographicalStateFromRoute from '@hooks/useGeographicalStateFromRoute'; +import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; +import styles from '@styles/styles'; import ROUTES from '@src/ROUTES'; -import useLocalize from '@hooks/useLocalize'; -import useGeographicalStateFromRoute from '@hooks/useGeographicalStateFromRoute'; -import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import FormHelpMessage from './FormHelpMessage'; +import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import refPropTypes from './refPropTypes'; const propTypes = { diff --git a/src/pages/settings/Profile/PersonalDetails/AddressPage.js b/src/pages/settings/Profile/PersonalDetails/AddressPage.js index a9b64b2420ed..752c3d2984a2 100644 --- a/src/pages/settings/Profile/PersonalDetails/AddressPage.js +++ b/src/pages/settings/Profile/PersonalDetails/AddressPage.js @@ -13,6 +13,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import StateSelector from '@components/StateSelector'; import TextInput from '@components/TextInput'; +import useGeographicalStateFromRoute from '@hooks/useGeographicalStateFromRoute'; import useLocalize from '@hooks/useLocalize'; import usePrivatePersonalDetails from '@hooks/usePrivatePersonalDetails'; import Navigation from '@libs/Navigation/Navigation'; @@ -22,7 +23,6 @@ import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import useGeographicalStateFromRoute from '@hooks/useGeographicalStateFromRoute'; const propTypes = { /* Onyx Props */ From d60ca6ccd76ca612d708d368b206364edb540400 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Jan 2024 01:06:47 +0100 Subject: [PATCH 0023/1082] remove unused split transaction draft --- src/components/MoneyRequestConfirmationList.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 13dce9337673..5807eb66f143 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -825,9 +825,6 @@ export default compose( key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, selector: DistanceRequestUtils.getDefaultMileageRate, }, - splitTransactionDraft: { - key: ({transactionID}) => `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`, - }, policy: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, }, From d849ea347c5fa1c488212e54e9f68411893d76c1 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Jan 2024 01:08:55 +0100 Subject: [PATCH 0024/1082] use StepScreenWrapper and backTo for back navigation --- .../step/IOURequestStepTaxAmountPage.js | 54 +++++++------------ 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js index 8ee3abb56d00..89f592b89e38 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js +++ b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js @@ -1,17 +1,11 @@ import {useFocusEffect} from '@react-navigation/native'; import React, {useCallback, useRef} from 'react'; -import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; import taxPropTypes from '@components/taxPropTypes'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as CurrencyUtils from '@libs/CurrencyUtils'; -import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as TransactionUtils from '@libs/TransactionUtils'; import MoneyRequestAmountForm from '@pages/iou/steps/MoneyRequestAmountForm'; @@ -21,6 +15,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes'; +import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; @@ -61,7 +56,6 @@ function IOURequestStepTaxAmountPage({ policyTaxRates, }) { const {translate} = useLocalize(); - const styles = useThemeStyles(); const textInput = useRef(null); const isEditing = Navigation.getActiveRoute().includes('taxAmount'); @@ -82,7 +76,7 @@ function IOURequestStepTaxAmountPage({ ); const navigateBack = () => { - Navigation.goBack(isEditing ? ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID) : ROUTES.HOME); + Navigation.goBack(backTo || ROUTES.HOME); }; const navigateToCurrencySelectionPage = () => { @@ -117,37 +111,25 @@ function IOURequestStepTaxAmountPage({ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); }; - const content = ( - (textInput.current = e)} - onCurrencyButtonPress={navigateToCurrencySelectionPage} - onSubmitButtonPress={updateTaxAmount} - /> - ); - return ( - - {({safeAreaPaddingBottomStyle}) => ( - - - - {content} - - - )} - + (textInput.current = e)} + onCurrencyButtonPress={navigateToCurrencySelectionPage} + onSubmitButtonPress={updateTaxAmount} + /> + ); } From f558497bacc30d13ae591bbc1667e6809b5b5419 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Jan 2024 01:09:10 +0100 Subject: [PATCH 0025/1082] use StepScreenWrapper and backTo for back navigation --- .../request/step/IOURequestStepTaxRatePage.js | 54 +++++++------------ 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepTaxRatePage.js b/src/pages/iou/request/step/IOURequestStepTaxRatePage.js index bae08cd8cb62..ba3c9c7a321c 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxRatePage.js +++ b/src/pages/iou/request/step/IOURequestStepTaxRatePage.js @@ -1,9 +1,6 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; import TaxPicker from '@components/TaxPicker'; import taxPropTypes from '@components/taxPropTypes'; import transactionPropTypes from '@components/transactionPropTypes'; @@ -16,21 +13,14 @@ import * as TransactionUtils from '@libs/TransactionUtils'; import * as IOU from '@userActions/IOU'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes'; +import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; const propTypes = { - /** Route from navigation */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** The type of IOU report, i.e. bill, request, send */ - iouType: PropTypes.string, - - /** The report ID of the IOU */ - reportID: PropTypes.string, - }), - }).isRequired, + /** Navigation route context info provided by react navigation */ + route: IOURequestStepRoutePropTypes.isRequired, /* Onyx Props */ /** Collection of tax rates attached to a policy */ @@ -52,16 +42,16 @@ const getTaxAmount = (taxRates, selectedTaxRate, amount) => { function IOURequestStepTaxRatePage({ route: { - params: {iouType, reportID}, + params: {iouType, reportID, backTo}, }, policyTaxRates, transaction, }) { const {translate} = useLocalize(); - function navigateBack() { - Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID)); - } + const navigateBack = () => { + Navigation.goBack(backTo || ROUTES.HOME); + }; const defaultTaxKey = policyTaxRates.defaultExternalID; const defaultTaxName = (defaultTaxKey && `${policyTaxRates.taxes[defaultTaxKey].name} (${policyTaxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; @@ -77,26 +67,18 @@ function IOURequestStepTaxRatePage({ }; return ( - - {({insets}) => ( - <> - navigateBack()} - /> - - - )} - + + ); } From de255d7a896b951659503bd7a483ee874d187dc8 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Jan 2024 09:16:34 +0100 Subject: [PATCH 0026/1082] remove unused transaction prop --- src/pages/iou/request/step/IOURequestStepTaxAmountPage.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js index 89f592b89e38..106119cff994 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js +++ b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js @@ -124,7 +124,6 @@ function IOURequestStepTaxAmountPage({ currency={currency} amount={transaction.taxAmount} taxAmount={getTaxAmount(transaction, policyTaxRates.defaultValue)} - transaction={transaction} ref={(e) => (textInput.current = e)} onCurrencyButtonPress={navigateToCurrencySelectionPage} onSubmitButtonPress={updateTaxAmount} From b1df50f1f0b0a9272d462484dd9827df7d9dc494 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Jan 2024 09:17:55 +0100 Subject: [PATCH 0027/1082] use policyTaxRates directly in taxPicker --- src/components/TaxPicker/index.js | 8 +++++++- src/pages/iou/request/step/IOURequestStepTaxRatePage.js | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/TaxPicker/index.js b/src/components/TaxPicker/index.js index f25a1b84bf64..287805692bcf 100644 --- a/src/components/TaxPicker/index.js +++ b/src/components/TaxPicker/index.js @@ -1,5 +1,6 @@ import lodashGet from 'lodash/get'; import React, {useMemo, useState} from 'react'; +import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import OptionsSelector from '@components/OptionsSelector'; import useLocalize from '@hooks/useLocalize'; @@ -8,6 +9,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import {defaultProps, propTypes} from './taxPickerPropTypes'; function TaxPicker({selectedTaxRate, policyTaxRates, insets, onSubmit}) { @@ -87,4 +89,8 @@ TaxPicker.displayName = 'TaxPicker'; TaxPicker.propTypes = propTypes; TaxPicker.defaultProps = defaultProps; -export default TaxPicker; +export default withOnyx({ + policyTaxRates: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAX_RATE}${policyID}`, + }, +})(TaxPicker); diff --git a/src/pages/iou/request/step/IOURequestStepTaxRatePage.js b/src/pages/iou/request/step/IOURequestStepTaxRatePage.js index ba3c9c7a321c..22f826d0457b 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxRatePage.js +++ b/src/pages/iou/request/step/IOURequestStepTaxRatePage.js @@ -10,6 +10,7 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; +import reportPropTypes from '@pages/reportPropTypes'; import * as IOU from '@userActions/IOU'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -28,9 +29,13 @@ const propTypes = { /** The transaction object being modified in Onyx */ transaction: transactionPropTypes, + + /** The report attached to the transaction */ + report: reportPropTypes, }; const defaultProps = { + report: {}, policyTaxRates: {}, transaction: {}, }; @@ -46,6 +51,7 @@ function IOURequestStepTaxRatePage({ }, policyTaxRates, transaction, + report, }) { const {translate} = useLocalize(); @@ -75,7 +81,7 @@ function IOURequestStepTaxRatePage({ > From 22c290eaddc9b2239995b034b84052810def0777 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Jan 2024 09:19:34 +0100 Subject: [PATCH 0028/1082] add EditRequestTaxAmount Page --- src/pages/EditRequestTaxAmountPage.js | 73 +++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/pages/EditRequestTaxAmountPage.js diff --git a/src/pages/EditRequestTaxAmountPage.js b/src/pages/EditRequestTaxAmountPage.js new file mode 100644 index 000000000000..2d10ccfe6f2b --- /dev/null +++ b/src/pages/EditRequestTaxAmountPage.js @@ -0,0 +1,73 @@ +import {useFocusEffect} from '@react-navigation/native'; +import PropTypes from 'prop-types'; +import React, {useCallback, useRef} from 'react'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; +import MoneyRequestAmountForm from './iou/steps/MoneyRequestAmountForm'; + +const propTypes = { + /** Transaction default amount value */ + defaultAmount: PropTypes.number.isRequired, + + /** Transaction default tax amount value */ + defaultTaxAmount: PropTypes.number.isRequired, + + /** Transaction default currency value */ + defaultCurrency: PropTypes.string.isRequired, + + /** Callback to fire when the Save button is pressed */ + onSubmit: PropTypes.func.isRequired, + + /** Callback to fire when we press on the currency */ + onNavigateToCurrency: PropTypes.func.isRequired, +}; + +function EditRequestTaxAmountPage({defaultAmount, defaultTaxAmount, defaultCurrency, onNavigateToCurrency, onSubmit}) { + const {translate} = useLocalize(); + const textInput = useRef(null); + const isEditing = Navigation.getActiveRoute().includes('taxAmount'); + + const focusTimeoutRef = useRef(null); + + useFocusEffect( + useCallback(() => { + focusTimeoutRef.current = setTimeout(() => textInput.current && textInput.current.focus(), CONST.ANIMATED_TRANSITION); + return () => { + if (!focusTimeoutRef.current) { + return; + } + clearTimeout(focusTimeoutRef.current); + }; + }, []), + ); + + return ( + + + (textInput.current = e)} + onCurrencyButtonPress={onNavigateToCurrency} + onSubmitButtonPress={onSubmit} + isEditing + /> + + ); +} + +EditRequestTaxAmountPage.propTypes = propTypes; +EditRequestTaxAmountPage.displayName = 'EditRequestTaxAmountPage'; + +export default EditRequestTaxAmountPage; From 42f79baa818863272e58e57ff6b87e7865c9b10b Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Jan 2024 09:19:59 +0100 Subject: [PATCH 0029/1082] add EditRequestTaxRate Page --- src/pages/EditRequestTaxRatePage.js | 47 +++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/pages/EditRequestTaxRatePage.js diff --git a/src/pages/EditRequestTaxRatePage.js b/src/pages/EditRequestTaxRatePage.js new file mode 100644 index 000000000000..965dfc1a65d7 --- /dev/null +++ b/src/pages/EditRequestTaxRatePage.js @@ -0,0 +1,47 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import TaxPicker from '@components/TaxPicker'; +import useLocalize from '@hooks/useLocalize'; +import Navigation from '@libs/Navigation/Navigation'; + +const propTypes = { + /** Transaction default tax Rate value */ + defaultTaxRate: PropTypes.string.isRequired, + + /** The policyID we are getting categories for */ + policyID: PropTypes.string.isRequired, + + /** Callback to fire when the Save button is pressed */ + onSubmit: PropTypes.func.isRequired, +}; + +function EditRequestTaxRatePage({defaultTaxRate, policyID, onSubmit}) { + const {translate} = useLocalize(); + + return ( + + {({insets}) => ( + <> + + + + )} + + ); +} + +EditRequestTaxRatePage.propTypes = propTypes; +EditRequestTaxRatePage.displayName = 'EditRequestTaxRatePage'; + +export default EditRequestTaxRatePage; From 3226d9cf8ba70e0218032197dd925d75e252faa3 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Jan 2024 09:23:47 +0100 Subject: [PATCH 0030/1082] add edit Tax amount and rate page to edit request page --- src/CONST.ts | 2 ++ src/pages/EditRequestPage.js | 39 +++++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index cec7cbc0b8a5..44bee149609f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1541,6 +1541,8 @@ const CONST = { RECEIPT: 'receipt', DISTANCE: 'distance', TAG: 'tag', + TAX_RATE: 'taxRate', + TAX_AMOUNT: 'taxAmount', }, FOOTER: { EXPENSE_MANAGEMENT_URL: `${USE_EXPENSIFY_URL}/expense-management`, diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index e41f30779f22..a43ddc44c492 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -27,6 +27,8 @@ import EditRequestDistancePage from './EditRequestDistancePage'; import EditRequestMerchantPage from './EditRequestMerchantPage'; import EditRequestReceiptPage from './EditRequestReceiptPage'; import EditRequestTagPage from './EditRequestTagPage'; +import EditRequestTaxAmountPage from './EditRequestTaxAmountPage'; +import EditRequestTaxRatePage from './EditRequestTaxRatePage'; import reportActionPropTypes from './home/report/reportActionPropTypes'; import reportPropTypes from './reportPropTypes'; @@ -61,6 +63,12 @@ const propTypes = { /** Transaction that stores the request data */ transaction: transactionPropTypes, + + /** The policy of the report */ + policy: PropTypes.shape({ + /** Is Tax tracking Enabled */ + isTaxTrackingEnabled: PropTypes.bool, + }), }; const defaultProps = { @@ -70,9 +78,10 @@ const defaultProps = { policyTags: {}, parentReportActions: {}, transaction: {}, + policy: {}, }; -function EditRequestPage({report, route, parentReport, policyCategories, policyTags, parentReportActions, transaction}) { +function EditRequestPage({report, policy, route, parentReport, policyCategories, policyTags, parentReportActions, transaction}) { const parentReportActionID = lodashGet(report, 'parentReportActionID', '0'); const parentReportAction = lodashGet(parentReportActions, parentReportActionID, {}); const { @@ -101,6 +110,9 @@ function EditRequestPage({report, route, parentReport, policyCategories, policyT // A flag for showing the tags page const shouldShowTags = isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledOptions(lodashValues(policyTagList))); + // A flag for showing tax rate + const shouldShowTax = isPolicyExpenseChat && policy.isTaxTrackingEnabled; + // Decides whether to allow or disallow editing a money request useEffect(() => { // Do not dismiss the modal, when a current user can edit this property of the money request. @@ -251,6 +263,28 @@ function EditRequestPage({report, route, parentReport, policyCategories, policyT ); } + if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.TAX_AMOUNT && shouldShowTax) { + return ( + {}} + onSubmit={() => {}} + /> + ); + } + + if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.TAX_RATE && shouldShowTax) { + return ( + {}} + /> + ); + } + if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT) { return ( `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, }, + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, + }, policyTags: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, }, From 9130f73d940ef8a8bb62fd3c8cf7914b15959dc2 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Jan 2024 09:24:32 +0100 Subject: [PATCH 0031/1082] add edit Tax amount and rate fields and route to pages --- .../ReportActionItem/MoneyRequestView.js | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 514dc71ffe2c..4c8cc0c3d131 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -12,6 +12,7 @@ import ReceiptEmptyState from '@components/ReceiptEmptyState'; import SpacerView from '@components/SpacerView'; import Switch from '@components/Switch'; import tagPropTypes from '@components/tagPropTypes'; +import taxPropTypes from '@components/taxPropTypes'; import Text from '@components/Text'; import transactionPropTypes from '@components/transactionPropTypes'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; @@ -64,6 +65,10 @@ const propTypes = { /** Collection of tags attached to a policy */ policyTags: tagPropTypes, + /* Onyx Props */ + /** Collection of tax rates attached to a policy */ + policyTaxRates: taxPropTypes, + ...withCurrentUserPersonalDetailsPropTypes, }; @@ -77,9 +82,10 @@ const defaultProps = { comment: {comment: ''}, }, policyTags: {}, + policyTaxRates: {}, }; -function MoneyRequestView({report, parentReport, parentReportActions, policyCategories, shouldShowHorizontalRule, transaction, policyTags, policy}) { +function MoneyRequestView({report, parentReport, parentReportActions, policyCategories, policyTaxRates, shouldShowHorizontalRule, transaction, policyTags, policy}) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -131,6 +137,9 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate const shouldShowTag = isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledOptions(lodashValues(policyTagsList))); const shouldShowBillable = isPolicyExpenseChat && (transactionBillable || !lodashGet(policy, 'disabledFields.defaultBillable', true)); + // A flag for showing tax rate + const shouldShowTax = isPolicyExpenseChat && policy.isTaxTrackingEnabled; + let amountDescription = `${translate('iou.amount')}`; if (isCardTransaction) { @@ -295,6 +304,31 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate /> )} + {shouldShowTax && ( + + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.TAX_RATE))} + /> + + )} + + {shouldShowTax && ( + + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.TAX_AMOUNT))} + /> + + )} {shouldShowBillable && ( {translate('common.billable')} @@ -333,6 +367,9 @@ export default compose( policyTags: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report.policyID}`, }, + policyTaxRates: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAX_RATE}${report.policyID}`, + }, parentReport: { key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`, }, From 4b8a102c3cef9510991b97f4d96082e20eb159e0 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Jan 2024 09:37:44 +0100 Subject: [PATCH 0032/1082] fix lint --- src/pages/EditRequestTaxAmountPage.js | 1 - src/pages/EditRequestTaxRatePage.js | 1 - 2 files changed, 2 deletions(-) diff --git a/src/pages/EditRequestTaxAmountPage.js b/src/pages/EditRequestTaxAmountPage.js index 2d10ccfe6f2b..dabfdfcb519f 100644 --- a/src/pages/EditRequestTaxAmountPage.js +++ b/src/pages/EditRequestTaxAmountPage.js @@ -54,7 +54,6 @@ function EditRequestTaxAmountPage({defaultAmount, defaultTaxAmount, defaultCurre > Date: Thu, 4 Jan 2024 09:42:40 +0100 Subject: [PATCH 0033/1082] remove is editing check --- src/pages/EditRequestTaxAmountPage.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/EditRequestTaxAmountPage.js b/src/pages/EditRequestTaxAmountPage.js index dabfdfcb519f..b5c20aa82d7c 100644 --- a/src/pages/EditRequestTaxAmountPage.js +++ b/src/pages/EditRequestTaxAmountPage.js @@ -29,7 +29,6 @@ const propTypes = { function EditRequestTaxAmountPage({defaultAmount, defaultTaxAmount, defaultCurrency, onNavigateToCurrency, onSubmit}) { const {translate} = useLocalize(); const textInput = useRef(null); - const isEditing = Navigation.getActiveRoute().includes('taxAmount'); const focusTimeoutRef = useRef(null); From 74f63f2eea05b7cb922d81867ecf95e82a7b45e4 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Jan 2024 13:28:12 +0100 Subject: [PATCH 0034/1082] complete types for policyTaxRates and Transaction --- src/types/onyx/PolicyTaxRates.ts | 25 +++++++++++++++++++++---- src/types/onyx/Transaction.ts | 3 +++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/types/onyx/PolicyTaxRates.ts b/src/types/onyx/PolicyTaxRates.ts index d549b620f51f..bc0905a0ff49 100644 --- a/src/types/onyx/PolicyTaxRates.ts +++ b/src/types/onyx/PolicyTaxRates.ts @@ -1,4 +1,4 @@ -type PolicyTaxRate = { +type TaxRate = { /** Name of a tax */ name: string; @@ -9,6 +9,23 @@ type PolicyTaxRate = { isDisabled?: boolean; }; -type PolicyTaxRates = Record; -export default PolicyTaxRate; -export type {PolicyTaxRates}; +type TaxRates = Record; +type PolicyTaxRates = { + /** Name of the tax */ + name: string; + + /** Default policy tax ID */ + defaultExternalID: string; + + /** Default value of taxes */ + defaultValue: string; + + /** Default foreign policy tax ID */ + foreignTaxDefault: string; + + /** List of tax names and values */ + taxes: TaxRates; +}; + +export default TaxRate; +export type {TaxRates, PolicyTaxRates}; diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 53bfc36a4e47..d67aec17a845 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -94,6 +94,9 @@ type Transaction = { /** If the transaction was made in a foreign currency, we send the original amount and currency */ originalAmount?: number; originalCurrency?: string; + taxRate?: { + text: string; + }; }; export default Transaction; From f2503f4e90b1456eb143f100b7b47159bde9be79 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Jan 2024 13:31:22 +0100 Subject: [PATCH 0035/1082] add getDefaultTaxName func. in TransactionUtils --- src/libs/TransactionUtils.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 615bea7ff18d..51a9f6ee188e 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -4,11 +4,12 @@ import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {RecentWaypoint, Report, ReportAction, Transaction} from '@src/types/onyx'; -import PolicyTaxRate, {PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; +import TaxRate, {PolicyTaxRates, TaxRates} from '@src/types/onyx/PolicyTaxRates'; import {Comment, Receipt, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; import {EmptyObject} from '@src/types/utils/EmptyObject'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; import DateUtils from './DateUtils'; +import * as Localize from './Localize'; import * as NumberUtils from './NumberUtils'; type AdditionalTransactionChanges = {comment?: string; waypoints?: WaypointCollection}; @@ -529,13 +530,24 @@ function calculateTaxAmount(percentage: string, amount: number) { /** * Calculates count of all tax enabled options */ -function getEnabledTaxRateCount(options: PolicyTaxRates) { - return Object.values(options).filter((option: PolicyTaxRate) => !option.isDisabled).length; +function getEnabledTaxRateCount(options: TaxRates) { + return Object.values(options).filter((option: TaxRate) => !option.isDisabled).length; +} + +/** + * Calculates get's the default tax name + */ +function getDefaultTaxName(policyTaxRates: PolicyTaxRates, transaction: Transaction) { + const defaultTaxKey = policyTaxRates.defaultExternalID; + const defaultTaxName = + (defaultTaxKey && `${policyTaxRates.taxes[defaultTaxKey].name} (${policyTaxRates.taxes[defaultTaxKey].value}) • ${Localize.translateLocal('common.default')}`) || ''; + return transaction?.taxRate?.text ?? defaultTaxName; } export { buildOptimisticTransaction, calculateTaxAmount, + getDefaultTaxName, getEnabledTaxRateCount, getUpdatedTransaction, getTransaction, From f10e413a895ac554ad7e1fe6f82a0ec290634c36 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Jan 2024 13:32:09 +0100 Subject: [PATCH 0036/1082] use getDefaultTaxName --- src/components/MoneyRequestConfirmationList.js | 5 +---- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 4 +--- src/pages/iou/request/step/IOURequestStepTaxRatePage.js | 4 +--- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 5807eb66f143..260c27205e94 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -262,10 +262,7 @@ function MoneyRequestConfirmationList(props) { props.isDistanceRequest ? currency : props.iouCurrencyCode, ); const formattedTaxAmount = CurrencyUtils.convertToDisplayString(props.transaction.taxAmount, props.iouCurrencyCode); - - const defaultTaxKey = props.policyTaxRates.defaultExternalID; - const defaultTaxName = (defaultTaxKey && `${props.policyTaxRates.taxes[defaultTaxKey].name} (${props.policyTaxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; - const taxRateTitle = (props.transaction.taxRate && props.transaction.taxRate.text) || defaultTaxName; + const taxRateTitle = TransactionUtils.getDefaultTaxName(props.policyTaxRates, transaction); const isFocused = useIsFocused(); const [formError, setFormError] = useState(''); diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 7ec95aec951f..55890c022179 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -292,9 +292,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ ); const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction.taxAmount, iouCurrencyCode); - const defaultTaxKey = policyTaxRates.defaultExternalID; - const defaultTaxName = (defaultTaxKey && `${policyTaxRates.taxes[defaultTaxKey].name} (${policyTaxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; - const taxRateTitle = (transaction.taxRate && transaction.taxRate.text) || defaultTaxName; + const taxRateTitle = TransactionUtils.getDefaultTaxName(policyTaxRates, transaction); const isFocused = useIsFocused(); const [formError, setFormError] = useState(''); diff --git a/src/pages/iou/request/step/IOURequestStepTaxRatePage.js b/src/pages/iou/request/step/IOURequestStepTaxRatePage.js index 22f826d0457b..ac25f2320684 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxRatePage.js +++ b/src/pages/iou/request/step/IOURequestStepTaxRatePage.js @@ -59,9 +59,7 @@ function IOURequestStepTaxRatePage({ Navigation.goBack(backTo || ROUTES.HOME); }; - const defaultTaxKey = policyTaxRates.defaultExternalID; - const defaultTaxName = (defaultTaxKey && `${policyTaxRates.taxes[defaultTaxKey].name} (${policyTaxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; - const selectedTaxRate = (transaction.taxRate && transaction.taxRate.text) || defaultTaxName; + const selectedTaxRate = TransactionUtils.getDefaultTaxName(policyTaxRates, transaction); const updateTaxRates = (taxes) => { const taxAmount = getTaxAmount(policyTaxRates, taxes.text, transaction.amount); From 13dccd87ddf5d6a5aa952aed3c5c8a549e9b47a9 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Jan 2024 13:32:33 +0100 Subject: [PATCH 0037/1082] remove unused Navigation import --- src/pages/EditRequestTaxAmountPage.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/EditRequestTaxAmountPage.js b/src/pages/EditRequestTaxAmountPage.js index b5c20aa82d7c..6a413bf12655 100644 --- a/src/pages/EditRequestTaxAmountPage.js +++ b/src/pages/EditRequestTaxAmountPage.js @@ -5,7 +5,6 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import MoneyRequestAmountForm from './iou/steps/MoneyRequestAmountForm'; From 7c154f116a91734baab62bbbfc6a480c7e9e9bf1 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Jan 2024 17:44:14 +0100 Subject: [PATCH 0038/1082] remove tax amount calculation implementation from requestStepAmount --- .../iou/request/step/IOURequestStepAmount.js | 47 +------------------ 1 file changed, 1 insertion(+), 46 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepAmount.js b/src/pages/iou/request/step/IOURequestStepAmount.js index 84e0ac8533c5..531d4d4d403b 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.js +++ b/src/pages/iou/request/step/IOURequestStepAmount.js @@ -1,21 +1,15 @@ import {useFocusEffect} from '@react-navigation/native'; -import PropTypes from 'prop-types'; import React, {useCallback, useRef} from 'react'; -import {withOnyx} from 'react-native-onyx'; -import taxPropTypes from '@components/taxPropTypes'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; import compose from '@libs/compose'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as ReportUtils from '@libs/ReportUtils'; import {getRequestType} from '@libs/TransactionUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; import MoneyRequestAmountForm from '@pages/iou/steps/MoneyRequestAmountForm'; import reportPropTypes from '@pages/reportPropTypes'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes'; import StepScreenWrapper from './StepScreenWrapper'; @@ -32,28 +26,11 @@ const propTypes = { /** The transaction object being modified in Onyx */ transaction: transactionPropTypes, - - /* Onyx Props */ - /** Collection of tax rates attached to a policy */ - policyTaxRates: taxPropTypes, - - /** The policy of the report */ - policy: PropTypes.shape({ - /** Is Tax tracking Enabled */ - isTaxTrackingEnabled: PropTypes.bool, - }), }; const defaultProps = { report: {}, transaction: {}, - policyTaxRates: {}, - policy: {}, -}; - -const getTaxAmount = (transaction, defaultTaxValue, amount) => { - const percentage = (transaction.taxRate ? transaction.taxRate.data.value : defaultTaxValue) || ''; - return TransactionUtils.calculateTaxAmount(percentage, amount); }; function IOURequestStepAmount({ @@ -63,8 +40,6 @@ function IOURequestStepAmount({ }, transaction, transaction: {currency: originalCurrency}, - policyTaxRates, - policy, }) { const {translate} = useLocalize(); const textInput = useRef(null); @@ -72,9 +47,6 @@ function IOURequestStepAmount({ const iouRequestType = getRequestType(transaction); const currency = selectedCurrency || originalCurrency; - const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)); - const isTaxTrackingEnabled = isPolicyExpenseChat && policy.isTaxTrackingEnabled; - useFocusEffect( useCallback(() => { focusTimeoutRef.current = setTimeout(() => textInput.current && textInput.current.focus(), CONST.ANIMATED_TRANSITION); @@ -101,12 +73,6 @@ function IOURequestStepAmount({ const navigateToNextPage = ({amount}) => { const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(amount)); - if ((iouRequestType === CONST.IOU.REQUEST_TYPE.MANUAL || backTo) && isTaxTrackingEnabled) { - const taxAmount = getTaxAmount(transaction, policyTaxRates.defaultValue, amountInSmallestCurrencyUnits); - const taxAmountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount)); - IOU.setMoneyRequestTaxAmount(transaction.transactionID, taxAmountInSmallestCurrencyUnits); - } - IOU.setMoneyRequestAmount_temporaryForRefactor(transactionID, amountInSmallestCurrencyUnits, currency || CONST.CURRENCY.USD); if (backTo) { @@ -153,15 +119,4 @@ IOURequestStepAmount.propTypes = propTypes; IOURequestStepAmount.defaultProps = defaultProps; IOURequestStepAmount.displayName = 'IOURequestStepAmount'; -export default compose( - withWritableReportOrNotFound, - withFullTransactionOrNotFound, - withOnyx({ - policyTaxRates: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAX_RATE}${report ? report.policyID : '0'}`, - }, - policy: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, - }, - }), -)(IOURequestStepAmount); +export default compose(withWritableReportOrNotFound, withFullTransactionOrNotFound)(IOURequestStepAmount); From d501ad75a419ea632796612cc5beb347706df0ac Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Jan 2024 17:44:50 +0100 Subject: [PATCH 0039/1082] set tax Amount --- ...poraryForRefactorRequestConfirmationList.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 55890c022179..79dd9e679263 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -8,6 +8,7 @@ import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; +import usePrevious from '@hooks/usePrevious'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; @@ -202,6 +203,11 @@ const defaultProps = { policyTaxRates: {}, }; +const getTaxAmount = (transaction, defaultTaxValue) => { + const percentage = (transaction.taxRate ? transaction.taxRate.data.value : defaultTaxValue) || ''; + return TransactionUtils.calculateTaxAmount(percentage, transaction.amount); +}; + function MoneyTemporaryForRefactorRequestConfirmationList({ bankAccountRoute, canModifyParticipants, @@ -294,6 +300,8 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const taxRateTitle = TransactionUtils.getDefaultTaxName(policyTaxRates, transaction); + const previousTransactionTaxAmount = usePrevious(transaction.taxAmount); + const isFocused = useIsFocused(); const [formError, setFormError] = useState(''); @@ -351,6 +359,16 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ IOU.setMoneyRequestAmount_temporaryForRefactor(transaction.transactionID, amount, currency); }, [shouldCalculateDistanceAmount, distance, rate, unit, transaction, currency]); + // calculate and set tax amount in transaction draft + useEffect(() => { + const taxAmount = getTaxAmount(transaction, policyTaxRates.defaultValue); + const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount)); + if (previousTransactionTaxAmount !== transaction.taxAmount && amountInSmallestCurrencyUnits !== transaction.taxAmount) { + return; + } + IOU.setMoneyRequestTaxAmount(transaction.transactionID, amountInSmallestCurrencyUnits); + }, [policyTaxRates.defaultValue, transaction, previousTransactionTaxAmount]); + /** * Returns the participants with amount * @param {Array} participants From 13deb831497581a1a314f42e975127cb6bde2883 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Thu, 4 Jan 2024 22:19:40 +0100 Subject: [PATCH 0040/1082] add Tax amount and Code API for updates --- src/libs/actions/IOU.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index b999435cc3e7..dc087ba42eac 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1051,6 +1051,36 @@ function updateMoneyRequestDate(transactionID, transactionThreadReportID, val) { API.write('UpdateMoneyRequestDate', params, onyxData); } +/** + * Updates the created tax amount of a money request + * + * @param {String} transactionID + * @param {String} optimisticReportActionID + * @param {Number} val + */ +function updateMoneyRequestTaxAmount(transactionID, optimisticReportActionID, val) { + const transactionChanges = { + taxAmount: val, + }; + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, optimisticReportActionID, transactionChanges, true); + API.write('UpdateMoneyRequestTaxAmount', params, onyxData); +} + +/** + * Updates the created tax rate of a money request + * + * @param {String} transactionID + * @param {String} optimisticReportActionID + * @param {String} val + */ +function updateMoneyRequestTaxRate(transactionID, optimisticReportActionID, val) { + const transactionChanges = { + taxCode: val, + }; + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, optimisticReportActionID, transactionChanges, true); + API.write('UpdateMoneyRequestTaxRate', params, onyxData); +} + /** * Edits an existing distance request * @@ -3490,6 +3520,8 @@ export { setUpDistanceTransaction, navigateToNextPage, updateMoneyRequestDate, + updateMoneyRequestTaxAmount, + updateMoneyRequestTaxRate, updateMoneyRequestAmountAndCurrency, replaceReceipt, detachReceipt, From fc3e59356aa037f68c525100a5b83358120fe0d9 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Mon, 8 Jan 2024 16:46:55 +0100 Subject: [PATCH 0041/1082] add function to check if tax rate has enabled options --- src/libs/OptionsListUtils.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 0dc12c720f31..3aa9ffc690bb 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -1058,6 +1058,17 @@ function getTagListSections(rawTags, recentlyUsedTags, selectedOptions, searchIn return tagSections; } +/** + * Verifies that there is at least one enabled option + * + * @param {Object[]} options - an initial strings array + * @property {boolean} [isDisabled] - Indicates if the tax rate is disabled. + * @returns {Boolean} + */ +function hasEnabledOptionsForTaxRate(options) { + return _.some(options, (option) => !option.isDisabled); +} + /** * Represents the data for a single tax rate. * @@ -1953,6 +1964,7 @@ export { getLastMessageTextForReport, getEnabledCategoriesCount, hasEnabledOptions, + hasEnabledOptionsForTaxRate, sortCategories, getCategoryOptionTree, formatMemberForList, From a6daa02f50f5ffb618e366dc6f10bbb3f7eedd4c Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Mon, 8 Jan 2024 16:48:15 +0100 Subject: [PATCH 0042/1082] add defaults for tax rate and amount and amount title --- .../ReportActionItem/MoneyRequestView.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 0b662f9d3578..a38b711e7192 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -155,6 +155,12 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate const isCardTransaction = TransactionUtils.isCardTransaction(transaction); const cardProgramName = isCardTransaction ? CardUtils.getCardDescription(transactionCardID) : ''; + const transactionTaxAmount = (transaction.taxAmount && transaction.taxAmount) || 0; + const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transactionTaxAmount, transactionCurrency); + + const transactionTaxCode = transaction.taxCode && transaction.taxCode; + const taxRateTitle = (transactionTaxCode && policyTaxRates.taxes[transactionTaxCode].name) || ''; + // Flags for allowing or disallowing editing a money request const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); const isCancelled = moneyRequestReport && moneyRequestReport.isCancelledIOU; @@ -181,7 +187,11 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate const shouldShowBillable = isPolicyExpenseChat && (transactionBillable || !lodashGet(policy, 'disabledFields.defaultBillable', true)); // A flag for showing tax rate - const shouldShowTax = isPolicyExpenseChat && policy.isTaxTrackingEnabled; + const shouldShowTax = + isPolicyExpenseChat && + policy && + policy.isTaxTrackingEnabled && + ((transactionTaxCode && transactionTaxAmount) || OptionsListUtils.hasEnabledOptionsForTaxRate(lodashValues(policyTaxRates.taxes))); const {getViolationsForField} = useViolations(transactionViolations); const hasViolations = useCallback((field) => canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]); @@ -359,7 +369,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate {shouldShowTax && ( Date: Mon, 8 Jan 2024 16:50:03 +0100 Subject: [PATCH 0043/1082] add defaults for tax rate and amount and amount title --- src/pages/EditRequestPage.js | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index 7e91b6787e5f..f47950eb71ff 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -7,6 +7,7 @@ import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView import categoryPropTypes from '@components/categoryPropTypes'; import ScreenWrapper from '@components/ScreenWrapper'; import tagPropTypes from '@components/tagPropTypes'; +import taxPropTypes from '@components/taxPropTypes'; import transactionPropTypes from '@components/transactionPropTypes'; import compose from '@libs/compose'; import * as CurrencyUtils from '@libs/CurrencyUtils'; @@ -66,6 +67,10 @@ const propTypes = { /** Is Tax tracking Enabled */ isTaxTrackingEnabled: PropTypes.bool, }), + + /* Onyx Props */ + /** Collection of tax rates attached to a policy */ + policyTaxRates: taxPropTypes, }; const defaultProps = { @@ -75,9 +80,10 @@ const defaultProps = { parentReportActions: {}, transaction: {}, policy: {}, + policyTaxRates: {}, }; -function EditRequestPage({report, policy, route, policyCategories, policyTags, parentReportActions, transaction}) { +function EditRequestPage({report, policy, policyTaxRates, route, policyCategories, policyTags, parentReportActions, transaction}) { const parentReportActionID = lodashGet(report, 'parentReportActionID', '0'); const parentReportAction = lodashGet(parentReportActions, parentReportActionID, {}); const { @@ -92,6 +98,11 @@ function EditRequestPage({report, policy, route, policyCategories, policyTags, p const defaultCurrency = lodashGet(route, 'params.currency', '') || transactionCurrency; const fieldToEdit = lodashGet(route, ['params', 'field'], ''); + const transactionTaxAmount = (transaction.taxAmount && transaction.taxAmount) || 0; + + const transactionTaxCode = transaction.taxCode && transaction.taxCode; + const taxRateTitle = (transactionTaxCode && policyTaxRates.taxes[transactionTaxCode].name) || ''; + // For now, it always defaults to the first tag of the policy const policyTag = PolicyUtils.getTag(policyTags); const policyTagList = lodashGet(policyTag, 'tags', {}); @@ -267,10 +278,13 @@ function EditRequestPage({report, policy, route, policyCategories, policyTags, p if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.TAX_AMOUNT && shouldShowTax) { return ( {}} + defaultAmount={transactionAmount} + defaultTaxAmount={transactionTaxAmount} + defaultCurrency={defaultCurrency} + onNavigateToCurrency={() => { + const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams()); + Navigation.navigate(ROUTES.EDIT_CURRENCY_REQUEST.getRoute(report.reportID, defaultCurrency, activeRoute)); + }} onSubmit={() => {}} /> ); @@ -279,7 +293,7 @@ function EditRequestPage({report, policy, route, policyCategories, policyTags, p if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.TAX_RATE && shouldShowTax) { return ( {}} /> @@ -336,6 +350,9 @@ export default compose( policyTags: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, }, + policyTaxRates: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAX_RATE}${report.policyID}`, + }, parentReportActions: { key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`, canEvict: false, From 24e7c072e69f5853ac6499d2d1345f7cf807bf7d Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Mon, 8 Jan 2024 17:17:19 +0100 Subject: [PATCH 0044/1082] add update functions to taxAmount and taxRate --- src/pages/EditRequestPage.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index f47950eb71ff..df13c727e430 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -139,6 +139,26 @@ function EditRequestPage({report, policy, policyTaxRates, route, policyCategorie Navigation.dismissModal(report.reportID); } + const updateTaxAmount = (transactionChanges) => { + if (transactionChanges.amount === transactionTaxAmount) { + return; + } + + const newTaxAmount = CurrencyUtils.convertToBackendAmount(Number.parseFloat(transactionChanges.amount)); + IOU.updateMoneyRequestTaxAmount(transaction.transactionID, report.reportID, newTaxAmount); + Navigation.dismissModal(report.reportID); + }; + + const updateTaxRate = (transactionChanges) => { + const newTaxCode = transactionChanges.data.code; + if (newTaxCode === transactionTaxCode) { + return; + } + + IOU.updateMoneyRequestTaxRate(transaction.transactionID, report.reportID, newTaxCode); + Navigation.dismissModal(report.reportID); + }; + const saveAmountAndCurrency = useCallback( ({amount, currency: newCurrency}) => { const newAmount = CurrencyUtils.convertToBackendAmount(Number.parseFloat(amount)); @@ -285,7 +305,7 @@ function EditRequestPage({report, policy, policyTaxRates, route, policyCategorie const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams()); Navigation.navigate(ROUTES.EDIT_CURRENCY_REQUEST.getRoute(report.reportID, defaultCurrency, activeRoute)); }} - onSubmit={() => {}} + onSubmit={updateTaxAmount} /> ); } @@ -295,7 +315,7 @@ function EditRequestPage({report, policy, policyTaxRates, route, policyCategorie {}} + onSubmit={updateTaxRate} /> ); } From 101aa5b69eceb36e96d8ba09d1208be8f369fbee Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Mon, 8 Jan 2024 19:59:18 +0100 Subject: [PATCH 0045/1082] update default amount and taxAmount --- src/pages/EditRequestPage.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index df13c727e430..3f346d649bca 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -82,7 +82,10 @@ const defaultProps = { policy: {}, policyTaxRates: {}, }; - +const getTaxAmount = (transaction, transactionTaxCode, policyTaxRates) => { + const percentage = (transactionTaxCode ? policyTaxRates.taxes[transactionTaxCode].value : policyTaxRates.defaultValue) || ''; + return CurrencyUtils.convertToBackendAmount(Number.parseFloat(TransactionUtils.calculateTaxAmount(percentage, transaction.amount))); +}; function EditRequestPage({report, policy, policyTaxRates, route, policyCategories, policyTags, parentReportActions, transaction}) { const parentReportActionID = lodashGet(report, 'parentReportActionID', '0'); const parentReportAction = lodashGet(parentReportActions, parentReportActionID, {}); @@ -298,8 +301,8 @@ function EditRequestPage({report, policy, policyTaxRates, route, policyCategorie if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.TAX_AMOUNT && shouldShowTax) { return ( { const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams()); From 61bf4767d02c4f00ed8eecd6f343567dad43a5dc Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Tue, 9 Jan 2024 07:25:51 +0100 Subject: [PATCH 0046/1082] use function for tax policy --- src/components/MoneyRequestConfirmationList.js | 5 +++-- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 3 ++- src/components/ReportActionItem/MoneyRequestView.js | 6 ++---- src/libs/PolicyUtils.ts | 5 +++++ src/pages/EditRequestPage.js | 5 +++-- src/types/onyx/Policy.ts | 3 +++ 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 260c27205e94..385d411427ba 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -20,6 +20,7 @@ import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import {isTaxPolicyEnabled} from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -247,8 +248,8 @@ function MoneyRequestConfirmationList(props) { // A flag for showing the tags field const shouldShowTags = props.isPolicyExpenseChat && (props.iouTag || OptionsListUtils.hasEnabledOptions(_.values(policyTagList))); - // A flag for showing tax fields - tax rate and tax amount - const shouldShowTax = props.isPolicyExpenseChat && props.policy.isTaxTrackingEnabled; + // A flag for showing tax rate + const shouldShowTax = isTaxPolicyEnabled(props.isPolicyExpenseChat, props.policy); // A flag for showing the billable field const shouldShowBillable = !lodashGet(props.policy, 'disabledFields.defaultBillable', true); diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 335a9e2f6801..5bd42ff11cf7 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -20,6 +20,7 @@ import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import {isTaxPolicyEnabled} from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -283,7 +284,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const shouldShowTags = isPolicyExpenseChat && OptionsListUtils.hasEnabledOptions(_.values(policyTagList)); // A flag for showing tax rate - const shouldShowTax = isPolicyExpenseChat && policy && policy.isTaxTrackingEnabled; + const shouldShowTax = isTaxPolicyEnabled(isPolicyExpenseChat, policy); // A flag for showing the billable field const shouldShowBillable = !lodashGet(policy, 'disabledFields.defaultBillable', true); diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index a38b711e7192..bd22c3f42415 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -30,6 +30,7 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import {isTaxPolicyEnabled} from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -188,10 +189,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate // A flag for showing tax rate const shouldShowTax = - isPolicyExpenseChat && - policy && - policy.isTaxTrackingEnabled && - ((transactionTaxCode && transactionTaxAmount) || OptionsListUtils.hasEnabledOptionsForTaxRate(lodashValues(policyTaxRates.taxes))); + isTaxPolicyEnabled(isPolicyExpenseChat, policy) && ((transactionTaxCode && transactionTaxAmount) || OptionsListUtils.hasEnabledOptionsForTaxRate(lodashValues(policyTaxRates.taxes))); const {getViolationsForField} = useViolations(transactionViolations); const hasViolations = useCallback((field) => canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]); diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 0cab97299324..831dc625cb95 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -202,6 +202,10 @@ function isPaidGroupPolicy(policy: OnyxEntry): boolean { return policy?.type === CONST.POLICY.TYPE.TEAM || policy?.type === CONST.POLICY.TYPE.CORPORATE; } +function isTaxPolicyEnabled(isPolicyExpenseChat: boolean, policy: OnyxEntry): boolean { + return (isPolicyExpenseChat && policy?.isTaxTrackingEnabled) ?? false; +} + export { getActivePolicies, hasPolicyMemberError, @@ -215,6 +219,7 @@ export { isExpensifyTeam, isExpensifyGuideTeam, isPolicyAdmin, + isTaxPolicyEnabled, getMemberAccountIDsForWorkspace, getIneligibleInvitees, getTag, diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index 7f9839f39111..8f1830e43a5f 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -14,6 +14,7 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import {isTaxPolicyEnabled} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import * as IOU from '@userActions/IOU'; @@ -83,7 +84,7 @@ const defaultProps = { policyTaxRates: {}, }; const getTaxAmount = (transaction, transactionTaxCode, policyTaxRates) => { - const percentage = (transactionTaxCode ? policyTaxRates.taxes[transactionTaxCode].value : policyTaxRates.defaultValue) || ''; + const percentage = (transactionTaxCode ? policyTaxRates.taxes[transactionTaxCode].value : policyTaxRates.defaultValue) || ''; return CurrencyUtils.convertToBackendAmount(Number.parseFloat(TransactionUtils.calculateTaxAmount(percentage, transaction.amount))); }; function EditRequestPage({report, policy, policyTaxRates, route, policyCategories, policyTags, parentReportActions, transaction}) { @@ -121,7 +122,7 @@ function EditRequestPage({report, policy, policyTaxRates, route, policyCategorie const shouldShowTags = isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledOptions(lodashValues(policyTagList))); // A flag for showing tax rate - const shouldShowTax = isPolicyExpenseChat && policy && policy.isTaxTrackingEnabled; + const shouldShowTax = isTaxPolicyEnabled(isPolicyExpenseChat, policy); // Decides whether to allow or disallow editing a money request useEffect(() => { diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index ff3a5e1dd23c..fc2eb02171da 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -65,6 +65,9 @@ type Policy = { /** Whether chat rooms can be created and used on this policy. Enabled manually by CQ/JS snippet. Always true for free policies. */ areChatRoomsEnabled: boolean; + /** Is Tax tracking Enabled */ + isTaxTrackingEnabled: boolean; + /** Whether policy expense chats can be created and used on this policy. Enabled manually by CQ/JS snippet. Always true for free policies. */ isPolicyExpenseChatEnabled: boolean; From e85912965de70ef3c933b9b2ed7c9d22a80f8671 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Tue, 9 Jan 2024 07:35:50 +0100 Subject: [PATCH 0047/1082] make isTaxTrackingEnabled optional --- src/types/onyx/Policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index fc2eb02171da..fc7f273de76d 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -66,7 +66,7 @@ type Policy = { areChatRoomsEnabled: boolean; /** Is Tax tracking Enabled */ - isTaxTrackingEnabled: boolean; + isTaxTrackingEnabled?: boolean; /** Whether policy expense chats can be created and used on this policy. Enabled manually by CQ/JS snippet. Always true for free policies. */ isPolicyExpenseChatEnabled: boolean; From fab239109cc035481c0429affee138a4ac7d01b8 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 10 Jan 2024 05:08:53 +0100 Subject: [PATCH 0048/1082] add tax amount and code type to Transaction types --- src/types/onyx/Transaction.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 399f1414db70..4aa61b8923c5 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -51,6 +51,8 @@ type Routes = Record; type Transaction = { amount: number; + taxAmount?: number; + taxCode?: string; billable: boolean; category: string; comment: Comment; From 9fd85f95026327b8f919252ba26f8320eb045bb2 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 10 Jan 2024 05:11:34 +0100 Subject: [PATCH 0049/1082] get tax amount abs value and tax code --- src/libs/TransactionUtils.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 5f8443b126c5..1b78d6f8ab6d 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -282,6 +282,27 @@ function getAmount(transaction: OnyxEntry, isFromExpenseReport: boo return amount ? -amount : 0; } +/** + * Return the tax amount field from the transaction. + */ +function getTaxAmount(transaction: OnyxEntry, isFromExpenseReport: boolean): number { + // IOU requests cannot have negative values but they can be stored as negative values, let's return absolute value + if (!isFromExpenseReport) { + return Math.abs(transaction?.taxAmount ?? 0); + } + + // To avoid -0 being shown, lets only change the sign if the value is other than 0. + const amount = transaction?.taxAmount ?? 0; + return amount ? -amount : 0; +} + +/** + * Return the tax code from the transaction. + */ +function getTaxCode(transaction: OnyxEntry): string { + return transaction?.taxCode ?? ''; +} + /** * Return the currency field from the transaction, return the modifiedCurrency if present. */ @@ -559,6 +580,8 @@ export { isManualRequest, isScanRequest, getAmount, + getTaxAmount, + getTaxCode, getCurrency, getDistance, getCardID, From 33cf5c438ec307cedb1fc1ef6b17691b046050b6 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 10 Jan 2024 05:13:32 +0100 Subject: [PATCH 0050/1082] add new fields in getTransactionDetails --- src/libs/ReportUtils.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0d7658adf180..e4d7cb728e82 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -281,6 +281,8 @@ type TransactionDetails = | { created: string; amount: number; + taxAmount?: number; + taxCode?: string; currency: string; merchant: string; waypoints?: WaypointCollection; @@ -1839,6 +1841,8 @@ function getTransactionDetails(transaction: OnyxEntry, createdDateF return { created: TransactionUtils.getCreated(transaction, createdDateFormat), amount: TransactionUtils.getAmount(transaction, isNotEmptyObject(report) && isExpenseReport(report)), + taxAmount: TransactionUtils.getTaxAmount(transaction, isNotEmptyObject(report) && isExpenseReport(report)), + taxCode: TransactionUtils.getTaxCode(transaction), currency: TransactionUtils.getCurrency(transaction), comment: TransactionUtils.getDescription(transaction), merchant: TransactionUtils.getMerchant(transaction), From 9d6e5b0372519213cf0b9dd92cd7d2a3370e6e9d Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 10 Jan 2024 05:16:06 +0100 Subject: [PATCH 0051/1082] get and use new tax amount and code fields from getTransactionDetails --- .../ReportActionItem/MoneyRequestView.js | 15 ++++++------ src/pages/EditRequestPage.js | 23 +++++++------------ 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 73bbda7bd3b7..c656ff45add7 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -135,6 +135,8 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate const { created: transactionDate, amount: transactionAmount, + taxAmount: transactionTaxAmount, + taxCode: transactionTaxCode, currency: transactionCurrency, comment: transactionDescription, merchant: transactionMerchant, @@ -156,11 +158,11 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate const isCardTransaction = TransactionUtils.isCardTransaction(transaction); const cardProgramName = isCardTransaction ? CardUtils.getCardDescription(transactionCardID) : ''; - const transactionTaxAmount = (transaction.taxAmount && transaction.taxAmount) || 0; - const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transactionTaxAmount, transactionCurrency); + const formattedTaxAmount = transactionTaxAmount ? CurrencyUtils.convertToDisplayString(transactionTaxAmount, transactionCurrency) : ''; - const transactionTaxCode = transaction.taxCode && transaction.taxCode; - const taxRateTitle = (transactionTaxCode && policyTaxRates.taxes[transactionTaxCode].name) || ''; + const taxName = `${policyTaxRates.taxes[transactionTaxCode].name}`; + const taxValue = `${policyTaxRates.taxes[transactionTaxCode].value}`; + const taxRateTitle = transactionTaxCode ? `${taxName} (${taxValue})` : ''; // Flags for allowing or disallowing editing a money request const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); @@ -188,8 +190,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate const shouldShowBillable = isPolicyExpenseChat && (transactionBillable || !lodashGet(policy, 'disabledFields.defaultBillable', true)); // A flag for showing tax rate - const shouldShowTax = - isTaxPolicyEnabled(isPolicyExpenseChat, policy) && ((transactionTaxCode && transactionTaxAmount) || OptionsListUtils.hasEnabledOptionsForTaxRate(lodashValues(policyTaxRates.taxes))); + const shouldShowTax = isTaxPolicyEnabled(isPolicyExpenseChat, policy) && transactionTaxCode && transactionTaxAmount; const {getViolationsForField} = useViolations(transactionViolations); const hasViolations = useCallback((field) => canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]); @@ -393,7 +394,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate {shouldShowTax && ( { +const getTaxAmount = (transactionAmount, transactionTaxCode, policyTaxRates) => { const percentage = (transactionTaxCode ? policyTaxRates.taxes[transactionTaxCode].value : policyTaxRates.defaultValue) || ''; - return CurrencyUtils.convertToBackendAmount(Number.parseFloat(TransactionUtils.calculateTaxAmount(percentage, transaction.amount))); + return CurrencyUtils.convertToBackendAmount(Number.parseFloat(TransactionUtils.calculateTaxAmount(percentage, transactionAmount))); }; function EditRequestPage({report, policy, policyTaxRates, route, policyCategories, policyTags, parentReportActions, transaction}) { const parentReportActionID = lodashGet(report, 'parentReportActionID', '0'); const parentReportAction = lodashGet(parentReportActions, parentReportActionID, {}); const { amount: transactionAmount, + taxAmount: transactionTaxAmount, + taxCode: transactionTaxCode, currency: transactionCurrency, comment: transactionDescription, merchant: transactionMerchant, @@ -102,10 +104,9 @@ function EditRequestPage({report, policy, policyTaxRates, route, policyCategorie const defaultCurrency = lodashGet(route, 'params.currency', '') || transactionCurrency; const fieldToEdit = lodashGet(route, ['params', 'field'], ''); - const transactionTaxAmount = (transaction.taxAmount && transaction.taxAmount) || 0; - - const transactionTaxCode = transaction.taxCode && transaction.taxCode; - const taxRateTitle = (transactionTaxCode && policyTaxRates.taxes[transactionTaxCode].name) || ''; + const taxName = `${policyTaxRates.taxes[transactionTaxCode].name}`; + const taxValue = `${policyTaxRates.taxes[transactionTaxCode].value}`; + const taxRateTitle = transactionTaxCode ? `${taxName} (${taxValue})` : ''; // For now, it always defaults to the first tag of the policy const policyTag = PolicyUtils.getTag(policyTags); @@ -144,10 +145,6 @@ function EditRequestPage({report, policy, policyTaxRates, route, policyCategorie } const updateTaxAmount = (transactionChanges) => { - if (transactionChanges.amount === transactionTaxAmount) { - return; - } - const newTaxAmount = CurrencyUtils.convertToBackendAmount(Number.parseFloat(transactionChanges.amount)); IOU.updateMoneyRequestTaxAmount(transaction.transactionID, report.reportID, newTaxAmount); Navigation.dismissModal(report.reportID); @@ -155,10 +152,6 @@ function EditRequestPage({report, policy, policyTaxRates, route, policyCategorie const updateTaxRate = (transactionChanges) => { const newTaxCode = transactionChanges.data.code; - if (newTaxCode === transactionTaxCode) { - return; - } - IOU.updateMoneyRequestTaxRate(transaction.transactionID, report.reportID, newTaxCode); Navigation.dismissModal(report.reportID); }; @@ -310,7 +303,7 @@ function EditRequestPage({report, policy, policyTaxRates, route, policyCategorie return ( { const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams()); From 25cb89edceac9386323076eb7eda8b53a5e9a17c Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 10 Jan 2024 05:29:28 +0100 Subject: [PATCH 0052/1082] add getTaxName to TransactionUtils --- src/libs/TransactionUtils.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 1b78d6f8ab6d..8aa9f05f813b 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -558,7 +558,7 @@ function getEnabledTaxRateCount(options: TaxRates) { } /** - * Calculates get's the default tax name + * Get's the default tax name */ function getDefaultTaxName(policyTaxRates: PolicyTaxRates, transaction: Transaction) { const defaultTaxKey = policyTaxRates.defaultExternalID; @@ -567,9 +567,19 @@ function getDefaultTaxName(policyTaxRates: PolicyTaxRates, transaction: Transact return transaction?.taxRate?.text ?? defaultTaxName; } +/** + * Get's the tax name + */ +function getTaxName(taxes: TaxRates, transactionTaxCode: string) { + const taxName = `${taxes[transactionTaxCode].name}`; + const taxValue = `${taxes[transactionTaxCode].value}`; + return transactionTaxCode ? `${taxName} (${taxValue})` : ''; +} + export { buildOptimisticTransaction, calculateTaxAmount, + getTaxName, getDefaultTaxName, getEnabledTaxRateCount, getUpdatedTransaction, From 9a9b1820bd7d3d8b157ad0d5cdc846a36a1bbbad Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 10 Jan 2024 05:30:35 +0100 Subject: [PATCH 0053/1082] use getTaxName --- src/components/ReportActionItem/MoneyRequestView.js | 4 +--- src/pages/EditRequestPage.js | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index c656ff45add7..60ed51d10506 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -160,9 +160,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate const formattedTaxAmount = transactionTaxAmount ? CurrencyUtils.convertToDisplayString(transactionTaxAmount, transactionCurrency) : ''; - const taxName = `${policyTaxRates.taxes[transactionTaxCode].name}`; - const taxValue = `${policyTaxRates.taxes[transactionTaxCode].value}`; - const taxRateTitle = transactionTaxCode ? `${taxName} (${taxValue})` : ''; + const taxRateTitle = TransactionUtils.getTaxName(policyTaxRates.taxes, transactionTaxCode); // Flags for allowing or disallowing editing a money request const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index a3375efbbdd5..7508e0796845 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -104,9 +104,7 @@ function EditRequestPage({report, policy, policyTaxRates, route, policyCategorie const defaultCurrency = lodashGet(route, 'params.currency', '') || transactionCurrency; const fieldToEdit = lodashGet(route, ['params', 'field'], ''); - const taxName = `${policyTaxRates.taxes[transactionTaxCode].name}`; - const taxValue = `${policyTaxRates.taxes[transactionTaxCode].value}`; - const taxRateTitle = transactionTaxCode ? `${taxName} (${taxValue})` : ''; + const taxRateTitle = TransactionUtils.getTaxName(policyTaxRates.taxes, transactionTaxCode); // For now, it always defaults to the first tag of the policy const policyTag = PolicyUtils.getTag(policyTags); From f723d0f68bf58f750df537c856f73845d95f044a Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 10 Jan 2024 13:45:33 +0100 Subject: [PATCH 0054/1082] Update src/components/MoneyTemporaryForRefactorRequestConfirmationList.js Co-authored-by: Michael (Mykhailo) Kravchenko --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 5bd42ff11cf7..85948dbaa530 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -360,7 +360,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ IOU.setMoneyRequestAmount_temporaryForRefactor(transaction.transactionID, amount, currency); }, [shouldCalculateDistanceAmount, distance, rate, unit, transaction, currency]); - // calculate and set tax amount in transaction draft + // Calculate and set tax amount in transaction draft useEffect(() => { const taxAmount = getTaxAmount(transaction, policyTaxRates.defaultValue); const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount)); From cc2831e8358a09c23db14511811df9141fcdde3c Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 10 Jan 2024 13:45:48 +0100 Subject: [PATCH 0055/1082] Update src/libs/TransactionUtils.ts Co-authored-by: Michael (Mykhailo) Kravchenko --- src/libs/TransactionUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 8aa9f05f813b..87c23974ab8a 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -568,7 +568,7 @@ function getDefaultTaxName(policyTaxRates: PolicyTaxRates, transaction: Transact } /** - * Get's the tax name + * Gets the tax name */ function getTaxName(taxes: TaxRates, transactionTaxCode: string) { const taxName = `${taxes[transactionTaxCode].name}`; From 1270f49460c6e83d4e429c868723f583da8fe4ce Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 10 Jan 2024 13:45:56 +0100 Subject: [PATCH 0056/1082] Update src/libs/TransactionUtils.ts Co-authored-by: Michael (Mykhailo) Kravchenko --- src/libs/TransactionUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 87c23974ab8a..64e3ab79ab2b 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -558,7 +558,7 @@ function getEnabledTaxRateCount(options: TaxRates) { } /** - * Get's the default tax name + * Gets the default tax name */ function getDefaultTaxName(policyTaxRates: PolicyTaxRates, transaction: Transaction) { const defaultTaxKey = policyTaxRates.defaultExternalID; From 37d651384db96233599b2410a582a35cca5173d0 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 10 Jan 2024 13:46:42 +0100 Subject: [PATCH 0057/1082] Update src/types/onyx/Policy.ts Co-authored-by: Michael (Mykhailo) Kravchenko --- src/types/onyx/Policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 5d315508f35a..3a7aacd5808a 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -62,7 +62,7 @@ type Policy = { /** The custom units data for this policy */ customUnits?: Record; - /** Is Tax tracking Enabled */ + /** Is tax tracking enabled */ isTaxTrackingEnabled?: boolean; /** Whether policy expense chats can be created and used on this policy. Enabled manually by CQ/JS snippet. Always true for free policies. */ From 584df427d6b35fc5c2b1e70e03ef88a9429320b2 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 10 Jan 2024 13:47:06 +0100 Subject: [PATCH 0058/1082] Update src/pages/EditRequestPage.js Co-authored-by: Michael (Mykhailo) Kravchenko --- src/pages/EditRequestPage.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index 7508e0796845..46fb719d26b5 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -62,14 +62,13 @@ const propTypes = { /** Transaction that stores the request data */ transaction: transactionPropTypes, - + /* Onyx Props */ /** The policy of the report */ policy: PropTypes.shape({ /** Is Tax tracking Enabled */ isTaxTrackingEnabled: PropTypes.bool, }), - /* Onyx Props */ /** Collection of tax rates attached to a policy */ policyTaxRates: taxPropTypes, }; From 1e8ffbc1da439bc23602f9bd67985dbbc0c51ed2 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Tue, 16 Jan 2024 10:59:42 +0100 Subject: [PATCH 0059/1082] fix prettier --- src/libs/actions/IOU.js | 44 ++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 47d959db9f16..816763037c3d 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1184,33 +1184,33 @@ function updateMoneyRequestTag(transactionID, transactionThreadReportID, tag) { } /** -* Updates the created tax amount of a money request -* -* @param {String} transactionID -* @param {String} optimisticReportActionID -* @param {Number} val -*/ + * Updates the created tax amount of a money request + * + * @param {String} transactionID + * @param {String} optimisticReportActionID + * @param {Number} val + */ function updateMoneyRequestTaxAmount(transactionID, optimisticReportActionID, val) { - const transactionChanges = { - taxAmount: val, - }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, optimisticReportActionID, transactionChanges, true); - API.write('UpdateMoneyRequestTaxAmount', params, onyxData); + const transactionChanges = { + taxAmount: val, + }; + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, optimisticReportActionID, transactionChanges, true); + API.write('UpdateMoneyRequestTaxAmount', params, onyxData); } /** -* Updates the created tax rate of a money request -* -* @param {String} transactionID -* @param {String} optimisticReportActionID -* @param {String} val -*/ + * Updates the created tax rate of a money request + * + * @param {String} transactionID + * @param {String} optimisticReportActionID + * @param {String} val + */ function updateMoneyRequestTaxRate(transactionID, optimisticReportActionID, val) { - const transactionChanges = { - taxCode: val, - }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, optimisticReportActionID, transactionChanges, true); - API.write('UpdateMoneyRequestTaxRate', params, onyxData); + const transactionChanges = { + taxCode: val, + }; + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, optimisticReportActionID, transactionChanges, true); + API.write('UpdateMoneyRequestTaxRate', params, onyxData); } /** From 404732f89f3c9c3032db907ef6b4acb47b5c9059 Mon Sep 17 00:00:00 2001 From: Aldo Canepa Date: Mon, 22 Jan 2024 13:34:48 -0300 Subject: [PATCH 0060/1082] Kill ReconnectToReport --- src/libs/actions/Report.ts | 59 ---------------------- src/pages/home/report/ReportActionsView.js | 29 +---------- 2 files changed, 2 insertions(+), 86 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 2ac85dfafa27..72b977d01e7d 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -767,58 +767,6 @@ function navigateToAndOpenChildReport(childReportID = '0', parentReportAction: P } } -/** Get the latest report history without marking the report as read. */ -function reconnect(reportID: string) { - const optimisticData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - reportName: allReports?.[reportID]?.reportName ?? CONST.REPORT.DEFAULT_REPORT_NAME, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - isLoadingInitialReportActions: true, - isLoadingNewerReportActions: false, - isLoadingOlderReportActions: false, - }, - }, - ]; - - const successData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - isLoadingInitialReportActions: false, - }, - }, - ]; - - const failureData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - isLoadingInitialReportActions: false, - }, - }, - ]; - - type ReconnectToReportParameters = { - reportID: string; - }; - - const parameters: ReconnectToReportParameters = { - reportID, - }; - - API.write('ReconnectToReport', parameters, {optimisticData, successData, failureData}); -} - /** * Gets the older actions that have not been read yet. * Normally happens when you scroll up on a chat, and the actions have not been read yet. @@ -1095,12 +1043,6 @@ function handleReportChanged(report: OnyxEntry) { conciergeChatReportID = report.reportID; } } - - // A report can be missing a name if a comment is received via pusher event and the report does not yet exist in Onyx (eg. a new DM created with the logged in person) - // In this case, we call reconnect so that we can fetch the report data without marking it as read - if (report.reportID && report.reportName === undefined) { - reconnect(report.reportID); - } } Onyx.connect({ @@ -2654,7 +2596,6 @@ export { searchInServer, addComment, addAttachment, - reconnect, updateWelcomeMessage, updateWriteCapabilityAndNavigate, updateNotificationPreference, diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 2758437a3962..d2ec0f124a62 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -88,10 +88,7 @@ function ReportActionsView(props) { const isFirstRender = useRef(true); const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); - - const prevNetworkRef = useRef(props.network); const prevAuthTokenType = usePrevious(props.session.authTokenType); - const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); const isFocused = useIsFocused(); @@ -118,36 +115,14 @@ function ReportActionsView(props) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useEffect(() => { - const prevNetwork = prevNetworkRef.current; - // When returning from offline to online state we want to trigger a request to OpenReport which - // will fetch the reportActions data and mark the report as read. If the report is not fully visible - // then we call ReconnectToReport which only loads the reportActions data without marking the report as read. - const wasNetworkChangeDetected = lodashGet(prevNetwork, 'isOffline') && !lodashGet(props.network, 'isOffline'); - if (wasNetworkChangeDetected) { - if (isReportFullyVisible) { - openReportIfNecessary(); - } else { - Report.reconnect(reportID); - } - } - // update ref with current network state - prevNetworkRef.current = props.network; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.network, props.report, isReportFullyVisible]); useEffect(() => { const wasLoginChangedDetected = prevAuthTokenType === 'anonymousAccount' && !props.session.authTokenType; if (wasLoginChangedDetected && didUserLogInDuringSession() && isUserCreatedPolicyRoom(props.report)) { - if (isReportFullyVisible) { - openReportIfNecessary(); - } else { - Report.reconnect(reportID); - } + openReportIfNecessary(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.session, props.report, isReportFullyVisible]); - + }, [props.session, props.report]); useEffect(() => { const prevIsSmallScreenWidth = prevIsSmallScreenWidthRef.current; // If the view is expanded from mobile to desktop layout From 0b4b6669a894f02f91af23fcac12e66e309e9494 Mon Sep 17 00:00:00 2001 From: Aldo Canepa Date: Mon, 22 Jan 2024 16:51:10 -0300 Subject: [PATCH 0061/1082] Lint --- src/pages/home/report/ReportActionsView.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index d2ec0f124a62..adb771ed4f81 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -115,7 +115,6 @@ function ReportActionsView(props) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useEffect(() => { const wasLoginChangedDetected = prevAuthTokenType === 'anonymousAccount' && !props.session.authTokenType; if (wasLoginChangedDetected && didUserLogInDuringSession() && isUserCreatedPolicyRoom(props.report)) { From fb08033cd73698250232de21554945f06a8f569e Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 31 Jan 2024 06:18:28 +0100 Subject: [PATCH 0062/1082] remove duplicate identifier --- src/types/onyx/Policy.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 23d7494f08cf..eca7e9d1ee06 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -76,9 +76,6 @@ type Policy = { /** The custom units data for this policy */ customUnits?: Record; - /** Is tax tracking enabled */ - isTaxTrackingEnabled?: boolean; - /** Whether chat rooms can be created and used on this policy. Enabled manually by CQ/JS snippet. Always true for free policies. */ areChatRoomsEnabled: boolean; From 70c02fd72cdb2cb34520bd5787c8bd40eef7796d Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 31 Jan 2024 06:37:34 +0100 Subject: [PATCH 0063/1082] fix lint and tsc --- src/libs/OptionsListUtils.ts | 10 ++-------- src/libs/TransactionUtils.ts | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index d550ae4305b0..51d1bc9699ad 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -4,7 +4,6 @@ import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import lodashOrderBy from 'lodash/orderBy'; import lodashSet from 'lodash/set'; -import lodashSome from 'lodash/some'; import lodashSortBy from 'lodash/sortBy'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; @@ -1148,13 +1147,9 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt /** * Verifies that there is at least one enabled option * - * @param {Object[]} options - an initial strings array - * @property {boolean} [isDisabled] - Indicates if the tax rate is disabled. - * @returns {Boolean} + * @param options - an initial strings array + * @returns boolean */ -function hasEnabledOptionsForTaxRate(options: TaxRates) { - return lodashSome(options, (option) => !option.isDisabled); -} /** * Transforms tax rates to a new object format - to add codes and new name with concatenated name and value. @@ -2001,7 +1996,6 @@ export { getLastMessageTextForReport, getEnabledCategoriesCount, hasEnabledOptions, - hasEnabledOptionsForTaxRate, sortCategories, getCategoryOptionTree, formatMemberForList, diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 711ff3fbbcd2..13a77183d1c5 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -5,7 +5,7 @@ import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {RecentWaypoint, Report, ReportAction, Transaction, TransactionViolation} from '@src/types/onyx'; -import type {TaxRate, TaxRates, PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; +import type {PolicyTaxRates, TaxRate, TaxRates} from '@src/types/onyx/PolicyTaxRates'; import type {Comment, Receipt, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; From 8bb0b3406cdb3a294766e2c875935d757c48dc86 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 31 Jan 2024 15:27:39 +0100 Subject: [PATCH 0064/1082] migrate EditRequestTaxAmountPage to tsc --- ...ntPage.js => EditRequestTaxAmountPage.tsx} | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) rename src/pages/{EditRequestTaxAmountPage.js => EditRequestTaxAmountPage.tsx} (79%) diff --git a/src/pages/EditRequestTaxAmountPage.js b/src/pages/EditRequestTaxAmountPage.tsx similarity index 79% rename from src/pages/EditRequestTaxAmountPage.js rename to src/pages/EditRequestTaxAmountPage.tsx index 6a413bf12655..7c99465fdca0 100644 --- a/src/pages/EditRequestTaxAmountPage.js +++ b/src/pages/EditRequestTaxAmountPage.tsx @@ -1,6 +1,6 @@ import {useFocusEffect} from '@react-navigation/native'; -import PropTypes from 'prop-types'; import React, {useCallback, useRef} from 'react'; +import {TextInput} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; @@ -8,28 +8,28 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; import MoneyRequestAmountForm from './iou/steps/MoneyRequestAmountForm'; -const propTypes = { +type EditRequestTaxAmountPageProps = { /** Transaction default amount value */ - defaultAmount: PropTypes.number.isRequired, + defaultAmount: number; /** Transaction default tax amount value */ - defaultTaxAmount: PropTypes.number.isRequired, + defaultTaxAmount: number; /** Transaction default currency value */ - defaultCurrency: PropTypes.string.isRequired, + defaultCurrency: string; /** Callback to fire when the Save button is pressed */ - onSubmit: PropTypes.func.isRequired, + onSubmit: () => void; /** Callback to fire when we press on the currency */ - onNavigateToCurrency: PropTypes.func.isRequired, + onNavigateToCurrency: () => void; }; -function EditRequestTaxAmountPage({defaultAmount, defaultTaxAmount, defaultCurrency, onNavigateToCurrency, onSubmit}) { +function EditRequestTaxAmountPage({defaultAmount, defaultTaxAmount, defaultCurrency, onNavigateToCurrency, onSubmit}: EditRequestTaxAmountPageProps) { const {translate} = useLocalize(); - const textInput = useRef(null); + const textInput = useRef(null); - const focusTimeoutRef = useRef(null); + const focusTimeoutRef = useRef(null); useFocusEffect( useCallback(() => { @@ -52,10 +52,11 @@ function EditRequestTaxAmountPage({defaultAmount, defaultTaxAmount, defaultCurre > (textInput.current = e)} + ref={textInput} onCurrencyButtonPress={onNavigateToCurrency} onSubmitButtonPress={onSubmit} isEditing @@ -64,7 +65,6 @@ function EditRequestTaxAmountPage({defaultAmount, defaultTaxAmount, defaultCurre ); } -EditRequestTaxAmountPage.propTypes = propTypes; EditRequestTaxAmountPage.displayName = 'EditRequestTaxAmountPage'; export default EditRequestTaxAmountPage; From 1f1df34f216dfce421e436f4e1282a5771982bab Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 31 Jan 2024 15:28:00 +0100 Subject: [PATCH 0065/1082] migrate EditRequestTaxRatePage to tsc --- ...uestTaxRatePage.js => EditRequestTaxRatePage.tsx} | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename src/pages/{EditRequestTaxRatePage.js => EditRequestTaxRatePage.tsx} (86%) diff --git a/src/pages/EditRequestTaxRatePage.js b/src/pages/EditRequestTaxRatePage.tsx similarity index 86% rename from src/pages/EditRequestTaxRatePage.js rename to src/pages/EditRequestTaxRatePage.tsx index 2a22ab087435..4991349d4225 100644 --- a/src/pages/EditRequestTaxRatePage.js +++ b/src/pages/EditRequestTaxRatePage.tsx @@ -5,18 +5,18 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TaxPicker from '@components/TaxPicker'; import useLocalize from '@hooks/useLocalize'; -const propTypes = { +type EditRequestTaxRatePageProps = { /** Transaction default tax Rate value */ - defaultTaxRate: PropTypes.string.isRequired, + defaultTaxRate: string, /** The policyID we are getting categories for */ - policyID: PropTypes.string.isRequired, + policyID: string, /** Callback to fire when the Save button is pressed */ - onSubmit: PropTypes.func.isRequired, + onSubmit: () => void, }; -function EditRequestTaxRatePage({defaultTaxRate, policyID, onSubmit}) { +function EditRequestTaxRatePage({defaultTaxRate, policyID, onSubmit}: EditRequestTaxRatePageProps) { const {translate} = useLocalize(); return ( @@ -29,6 +29,7 @@ function EditRequestTaxRatePage({defaultTaxRate, policyID, onSubmit}) { <> Date: Wed, 31 Jan 2024 15:34:36 +0100 Subject: [PATCH 0066/1082] fix tsc errors --- src/pages/EditRequestTaxAmountPage.tsx | 4 ++-- src/pages/EditRequestTaxRatePage.tsx | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pages/EditRequestTaxAmountPage.tsx b/src/pages/EditRequestTaxAmountPage.tsx index 7c99465fdca0..437e84df652c 100644 --- a/src/pages/EditRequestTaxAmountPage.tsx +++ b/src/pages/EditRequestTaxAmountPage.tsx @@ -1,6 +1,6 @@ import {useFocusEffect} from '@react-navigation/native'; import React, {useCallback, useRef} from 'react'; -import {TextInput} from 'react-native'; +import type {TextInput} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; @@ -52,7 +52,7 @@ function EditRequestTaxAmountPage({defaultAmount, defaultTaxAmount, defaultCurre > void, + onSubmit: () => void; }; function EditRequestTaxRatePage({defaultTaxRate, policyID, onSubmit}: EditRequestTaxRatePageProps) { @@ -29,7 +28,7 @@ function EditRequestTaxRatePage({defaultTaxRate, policyID, onSubmit}: EditReques <> Date: Wed, 31 Jan 2024 15:35:25 +0100 Subject: [PATCH 0067/1082] fix lint --- src/pages/EditRequestTaxRatePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/EditRequestTaxRatePage.tsx b/src/pages/EditRequestTaxRatePage.tsx index 778f25ae0d98..1c1dc8219ae2 100644 --- a/src/pages/EditRequestTaxRatePage.tsx +++ b/src/pages/EditRequestTaxRatePage.tsx @@ -28,7 +28,7 @@ function EditRequestTaxRatePage({defaultTaxRate, policyID, onSubmit}: EditReques <> Date: Wed, 31 Jan 2024 12:58:51 -0800 Subject: [PATCH 0068/1082] Delete unused params for deleted command ReconnectToReport --- src/libs/API/parameters/ReconnectToReportParams.ts | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 src/libs/API/parameters/ReconnectToReportParams.ts diff --git a/src/libs/API/parameters/ReconnectToReportParams.ts b/src/libs/API/parameters/ReconnectToReportParams.ts deleted file mode 100644 index e7701cd36ca9..000000000000 --- a/src/libs/API/parameters/ReconnectToReportParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -type ReconnectToReportParams = { - reportID: string; -}; - -export default ReconnectToReportParams; From 10360a32be5e48825a62ce4fd058f494a5020cc1 Mon Sep 17 00:00:00 2001 From: Aldo Canepa Date: Wed, 31 Jan 2024 15:21:02 -0800 Subject: [PATCH 0069/1082] Remove ReconnectToReportParams --- src/libs/API/parameters/index.ts | 1 - src/libs/API/types.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 039398c0fbf6..0fd627ec7f9f 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -76,7 +76,6 @@ export type {default as VerifyIdentityForBankAccountParams} from './VerifyIdenti export type {default as AnswerQuestionsForWalletParams} from './AnswerQuestionsForWalletParams'; export type {default as AddCommentOrAttachementParams} from './AddCommentOrAttachementParams'; export type {default as OptInOutToPushNotificationsParams} from './OptInOutToPushNotificationsParams'; -export type {default as ReconnectToReportParams} from './ReconnectToReportParams'; export type {default as ReadNewestActionParams} from './ReadNewestActionParams'; export type {default as MarkAsUnreadParams} from './MarkAsUnreadParams'; export type {default as TogglePinnedChatParams} from './TogglePinnedChatParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index f58ebc30b4a2..f17645a3e3ac 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -185,7 +185,6 @@ type WriteCommandParameters = { [WRITE_COMMANDS.ADD_PERSONAL_BANK_ACCOUNT]: Parameters.AddPersonalBankAccountParams; [WRITE_COMMANDS.OPT_IN_TO_PUSH_NOTIFICATIONS]: Parameters.OptInOutToPushNotificationsParams; [WRITE_COMMANDS.OPT_OUT_OF_PUSH_NOTIFICATIONS]: Parameters.OptInOutToPushNotificationsParams; - [WRITE_COMMANDS.RECONNECT_TO_REPORT]: Parameters.ReconnectToReportParams; [WRITE_COMMANDS.READ_NEWEST_ACTION]: Parameters.ReadNewestActionParams; [WRITE_COMMANDS.MARK_AS_UNREAD]: Parameters.MarkAsUnreadParams; [WRITE_COMMANDS.TOGGLE_PINNED_CHAT]: Parameters.TogglePinnedChatParams; From 1457b8b34ab9667d813da0da1a933e917bff57bd Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Thu, 1 Feb 2024 23:27:24 +0800 Subject: [PATCH 0070/1082] fix emoji picker keyboard issue --- src/CONST.ts | 9 + src/components/EmojiPicker/EmojiPicker.js | 1 + src/components/Modal/BaseModal.tsx | 35 ++- src/components/Modal/ModalContent.tsx | 19 ++ src/components/Modal/index.android.tsx | 10 - src/components/Modal/types.ts | 9 + src/libs/ComposerFocusManager.ts | 273 +++++++++++++++++- src/libs/focusComposerWithDelay.ts | 15 +- .../isWindowReadyToFocus/index.android.ts | 27 ++ src/libs/isWindowReadyToFocus/index.ts | 3 + 10 files changed, 363 insertions(+), 38 deletions(-) create mode 100644 src/components/Modal/ModalContent.tsx create mode 100644 src/libs/isWindowReadyToFocus/index.android.ts create mode 100644 src/libs/isWindowReadyToFocus/index.ts diff --git a/src/CONST.ts b/src/CONST.ts index 1ccdfd9a82a8..4c5523790a91 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -718,6 +718,15 @@ const CONST = { RIGHT: 'right', }, POPOVER_MENU_PADDING: 8, + BUSINESS_TYPE: { + DEFAULT: 'default', + ATTACHMENT: 'attachment', + }, + RESTORE_FOCUS_TYPE: { + DEFAULT: 'default', + DELETE: 'delete', + PRESERVE: 'preserve', + }, }, TIMING: { CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION: 'calc_most_recent_last_modified_action', diff --git a/src/components/EmojiPicker/EmojiPicker.js b/src/components/EmojiPicker/EmojiPicker.js index eaf89b7f64ea..10d040301e94 100644 --- a/src/components/EmojiPicker/EmojiPicker.js +++ b/src/components/EmojiPicker/EmojiPicker.js @@ -187,6 +187,7 @@ const EmojiPicker = forwardRef((props, ref) => { outerStyle={StyleUtils.getOuterModalStyle(windowHeight, props.viewportOffsetTop)} innerContainerStyle={styles.popoverInnerContainer} avoidKeyboard + shouldEnableNewFocusManagement > , ) { @@ -55,6 +58,14 @@ function BaseModal( const isVisibleRef = useRef(isVisible); const wasVisible = usePrevious(isVisible); + const modalId = useMemo(() => ComposerFocusManager.getId(), []); + const saveFocusState = () => { + if (shouldEnableNewFocusManagement) { + ComposerFocusManager.saveFocusState(modalId); + } + ComposerFocusManager.resetReadyToFocus(modalId); + }; + /** * Hides modal * @param callHideCallback - Should we call the onModalHide callback @@ -69,11 +80,9 @@ function BaseModal( onModalHide(); } Modal.onModalDidClose(); - if (!fullscreen) { - ComposerFocusManager.setReadyToFocus(); - } + ComposerFocusManager.tryRestoreFocusAfterClosedCompletely(modalId, restoreFocusType); }, - [shouldSetModalVisibility, onModalHide, fullscreen], + [shouldSetModalVisibility, onModalHide, restoreFocusType, modalId], ); useEffect(() => { @@ -121,7 +130,7 @@ function BaseModal( }; const handleDismissModal = () => { - ComposerFocusManager.setReadyToFocus(); + ComposerFocusManager.setReadyToFocus(modalId); }; const { @@ -183,7 +192,7 @@ function BaseModal( onModalShow={handleShowModal} propagateSwipe={propagateSwipe} onModalHide={hideModal} - onModalWillShow={() => ComposerFocusManager.resetReadyToFocus()} + onModalWillShow={saveFocusState} onDismiss={handleDismissModal} onSwipeComplete={() => onClose?.()} swipeDirection={swipeDirection} @@ -207,12 +216,14 @@ function BaseModal( avoidKeyboard={avoidKeyboard} customBackdrop={shouldUseCustomBackdrop ? : undefined} > - - {children} - + + + {children} + + ); } diff --git a/src/components/Modal/ModalContent.tsx b/src/components/Modal/ModalContent.tsx new file mode 100644 index 000000000000..5c8e0d2ece6b --- /dev/null +++ b/src/components/Modal/ModalContent.tsx @@ -0,0 +1,19 @@ +import type {ReactNode} from 'react'; +import React from 'react'; + +type ModalContentProps = { + /** Modal contents */ + children: ReactNode; + + /** called after modal content is dismissed */ + onDismiss: () => void; +}; + +function ModalContent({children, onDismiss = () => {}}: ModalContentProps) { + // eslint-disable-next-line react-hooks/exhaustive-deps + React.useEffect(() => () => onDismiss?.(), []); + return children; +} +ModalContent.displayName = 'ModalContent'; + +export default ModalContent; diff --git a/src/components/Modal/index.android.tsx b/src/components/Modal/index.android.tsx index 86a1fd272185..7cb2c6083752 100644 --- a/src/components/Modal/index.android.tsx +++ b/src/components/Modal/index.android.tsx @@ -1,17 +1,7 @@ import React from 'react'; -import {AppState} from 'react-native'; -import ComposerFocusManager from '@libs/ComposerFocusManager'; import BaseModal from './BaseModal'; import type BaseModalProps from './types'; -AppState.addEventListener('focus', () => { - ComposerFocusManager.setReadyToFocus(); -}); - -AppState.addEventListener('blur', () => { - ComposerFocusManager.resetReadyToFocus(); -}); - // Only want to use useNativeDriver on Android. It has strange flashes issue on IOS // https://github.com/react-native-modal/react-native-modal#the-modal-flashes-in-a-weird-way-when-animating function Modal({useNativeDriver = true, ...rest}: BaseModalProps) { diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index a0cdb737d448..43a2c281415a 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -62,6 +62,15 @@ type BaseModalProps = Partial & { /** Should we use a custom backdrop for the modal? (This prevents focus issues on desktop) */ shouldUseCustomBackdrop?: boolean; + + /** + * Whether the modal should enable the new focus manager. + * We are attempting to migrate to a new refocus manager, adding this property for gradual migration. + * */ + shouldEnableNewFocusManagement?: boolean; + + /** How to re-focus after the modal is dismissed */ + restoreFocusType?: ValueOf; }; export default BaseModalProps; diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index b66bbe92599e..88e701a2e569 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -1,25 +1,278 @@ -let isReadyToFocusPromise = Promise.resolve(); -let resolveIsReadyToFocus: (value: void | PromiseLike) => void; +import type {View} from 'react-native'; +import {TextInput} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; +import isWindowReadyToFocus from './isWindowReadyToFocus'; -function resetReadyToFocus() { - isReadyToFocusPromise = new Promise((resolve) => { - resolveIsReadyToFocus = resolve; +type ModalId = number | undefined; + +type InputElement = (TextInput & HTMLElement) | null; + +/** + * So far, modern browsers only support the file cancel event in some newer versions + * (i.e., Chrome: 113+ / Firefox: 91+ / Safari 16.4+), and there is no standard feature detection method available. + * We will introduce this prop to isolate the impact of the file upload modal on the focus stack. + */ +type BusinessType = ValueOf | undefined; + +type RestoreFocusType = ValueOf | undefined; + +type ModalContainer = View | HTMLElement | undefined | null; + +type FocusMapValue = { + input: InputElement; + businessType?: BusinessType; +}; + +type PromiseMapValue = { + ready: Promise; + resolve: () => void; +}; + +let focusedInput: InputElement = null; +let uniqueModalId = 1; +const focusMap = new Map(); +const activeModals: ModalId[] = []; +const promiseMap = new Map(); + +/** + * react-native-web doesn't support `currentlyFocusedInput`, so we need to make it compatible. + */ +function getActiveInput() { + return (TextInput.State.currentlyFocusedInput ? TextInput.State.currentlyFocusedInput() : TextInput.State.currentlyFocusedField()) as InputElement; +} + +/** + * On web platform, if the modal is displayed by a click, the blur event is fired before the modal appears, + * so we need to cache the focused input in the pointerdown handler, which is fired before the blur event. + */ +function saveFocusedInput() { + focusedInput = getActiveInput(); +} + +/** + * If a click does not display the modal, we also should clear the cached value to avoid potential issues. + */ +function clearFocusedInput() { + if (!focusedInput) { + return; + } + + // we have to use timeout because of measureLayout + setTimeout(() => (focusedInput = null), CONST.ANIMATION_IN_TIMING); +} + +/** + * When a TextInput is unmounted, we also should release the reference here to avoid potential issues. + * + */ +function releaseInput(input: InputElement) { + if (!input) { + return; + } + if (input === focusedInput) { + focusedInput = null; + } + [...focusMap].forEach(([key, value]) => { + if (value.input !== input) { + return; + } + focusMap.delete(key); + }); +} + +function getId() { + return uniqueModalId++; +} + +/** + * Save the focus state when opening the modal. + */ +function saveFocusState(id: ModalId, businessType: BusinessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, shouldClearFocusWithType = false, container: ModalContainer = undefined) { + const activeInput = getActiveInput(); + + // For popoverWithoutOverlay, react calls autofocus before useEffect. + const input = focusedInput ?? activeInput; + focusedInput = null; + if (activeModals.indexOf(id) < 0) { + activeModals.push(id); + } + + if (shouldClearFocusWithType) { + [...focusMap].forEach(([key, value]) => { + if (value.businessType !== businessType) { + return; + } + focusMap.delete(key); + }); + } + + if (container && 'contains' in container && container.contains(input)) { + return; + } + focusMap.set(id, {input, businessType}); + if (!input) { + return; + } + input.blur(); +} + +/** + * On web platform, if we intentionally click on another input box, there is no need to restore focus. + * Additionally, if we are closing the RHP, we can ignore the focused input. + */ +function focus(input: InputElement, shouldIgnoreFocused = false) { + if (!input) { + return; + } + if (shouldIgnoreFocused) { + isWindowReadyToFocus().then(() => input.focus()); + return; + } + const activeInput = getActiveInput(); + if (activeInput) { + return; + } + isWindowReadyToFocus().then(() => input.focus()); +} + +/** + * Restore the focus state after the modal is dismissed. + */ +function restoreFocusState( + id: ModalId, + shouldIgnoreFocused = false, + restoreFocusType: RestoreFocusType = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT, + businessType: BusinessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, +) { + if (!id) { + return; + } + + // The stack is empty + if (activeModals.length < 1) { + return; + } + const index = activeModals.indexOf(id); + + // This id has been removed from the stack. + if (index < 0) { + return; + } + activeModals.splice(index, 1); + if (restoreFocusType === CONST.MODAL.RESTORE_FOCUS_TYPE.PRESERVE) { + return; + } + + const {input} = focusMap.get(id) ?? {}; + focusMap.delete(id); + if (restoreFocusType === CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE) { + return; + } + + // This modal is not the topmost one, do not restore it. + if (activeModals.length > index) { + if (input) { + const lastId = activeModals.slice(-1)[0]; + focusMap.set(lastId, {...focusMap.get(lastId), input}); + } + return; + } + if (input) { + focus(input, shouldIgnoreFocused); + return; + } + + // Try to find the topmost one and restore it + const stack = [...focusMap].filter(([, v]) => v.input && v.businessType === businessType); + if (stack.length < 1) { + return; + } + const [lastId, {input: lastInput}] = stack.slice(-1)[0]; + + // The previous modal is still active + if (activeModals.indexOf(lastId) >= 0) { + return; + } + focus(lastInput, shouldIgnoreFocused); + focusMap.delete(lastId); +} + +function resetReadyToFocus(id: ModalId) { + const promise: PromiseMapValue = { + ready: Promise.resolve(), + resolve: () => {}, + }; + promise.ready = new Promise((resolve) => { + promise.resolve = resolve; }); + promiseMap.set(id, promise); +} + +/** + * Backward compatibility, for cases without an ID, it's fine to just take the topmost one. + */ +function getKey(id: ModalId) { + if (id) { + return id; + } + if (promiseMap.size < 1) { + return 0; + } + return [...promiseMap.keys()].slice(-1)[0]; } -function setReadyToFocus() { - if (!resolveIsReadyToFocus) { +function setReadyToFocus(id?: ModalId) { + const key = getKey(id); + const promise = promiseMap.get(key); + if (!promise) { return; } - resolveIsReadyToFocus(); + promise.resolve?.(); + promiseMap.delete(key); } -function isReadyToFocus(): Promise { - return isReadyToFocusPromise; +function isReadyToFocus(id?: ModalId) { + const key = getKey(id); + const promise = promiseMap.get(key); + if (!promise) { + return Promise.resolve(); + } + return promise.ready; +} + +function tryRestoreFocusAfterClosedCompletely(id: ModalId, restoreType: RestoreFocusType, businessType?: BusinessType) { + isReadyToFocus(id)?.then(() => restoreFocusState(id, false, restoreType, businessType)); +} + +/** + * So far, this will only be called in file canceled event handler. + */ +function tryRestoreFocusByExternal(businessType: BusinessType) { + const stack = [...focusMap].filter(([, value]) => value.businessType === businessType && value.input); + if (stack.length < 1) { + return; + } + const [key, {input}] = stack.slice(-1)[0]; + focusMap.delete(key); + if (!input) { + return; + } + focus(input); } +export type {InputElement}; + export default { + getId, + saveFocusedInput, + clearFocusedInput, + releaseInput, + saveFocusState, + restoreFocusState, resetReadyToFocus, setReadyToFocus, isReadyToFocus, + tryRestoreFocusAfterClosedCompletely, + tryRestoreFocusByExternal, }; diff --git a/src/libs/focusComposerWithDelay.ts b/src/libs/focusComposerWithDelay.ts index 6a2f85f7d311..a61c45325b3b 100644 --- a/src/libs/focusComposerWithDelay.ts +++ b/src/libs/focusComposerWithDelay.ts @@ -1,6 +1,7 @@ import type {TextInput} from 'react-native'; import * as EmojiPickerAction from './actions/EmojiPickerAction'; import ComposerFocusManager from './ComposerFocusManager'; +import isWindowReadyToFocus from './isWindowReadyToFocus'; type FocusComposerWithDelay = (shouldDelay?: boolean) => void; /** @@ -22,12 +23,14 @@ function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithD textInput.focus(); return; } - ComposerFocusManager.isReadyToFocus().then(() => { - if (!textInput) { - return; - } - textInput.focus(); - }); + ComposerFocusManager.isReadyToFocus() + .then(isWindowReadyToFocus) + .then(() => { + if (!textInput) { + return; + } + textInput.focus(); + }); }; } diff --git a/src/libs/isWindowReadyToFocus/index.android.ts b/src/libs/isWindowReadyToFocus/index.android.ts new file mode 100644 index 000000000000..b9cca1b5a294 --- /dev/null +++ b/src/libs/isWindowReadyToFocus/index.android.ts @@ -0,0 +1,27 @@ +import {AppState} from 'react-native'; + +let isWindowReadyPromise = Promise.resolve(); +let resolveWindowReadyToFocus: () => void; + +AppState.addEventListener('focus', () => { + if (!resolveWindowReadyToFocus) { + return; + } + resolveWindowReadyToFocus(); +}); + +AppState.addEventListener('blur', () => { + isWindowReadyPromise = new Promise((resolve) => { + resolveWindowReadyToFocus = resolve; + }); +}); + +/** + * If we want to show the soft keyboard reliably, we need to ensure that the input's window gains focus first. + * Fortunately, we only need to manage the focus of the app window now, + * so we can achieve this by listening to the 'focus' event of the AppState. + * See {@link https://developer.android.com/develop/ui/views/touch-and-input/keyboard-input/visibility#ShowReliably} + */ +const isWindowReadyToFocus = () => isWindowReadyPromise; + +export default isWindowReadyToFocus; diff --git a/src/libs/isWindowReadyToFocus/index.ts b/src/libs/isWindowReadyToFocus/index.ts new file mode 100644 index 000000000000..7ae3930c0c1d --- /dev/null +++ b/src/libs/isWindowReadyToFocus/index.ts @@ -0,0 +1,3 @@ +const isWindowReadyToFocus = () => Promise.resolve(); + +export default isWindowReadyToFocus; From 70e763c4652f0c29454b2658e3e096bea23b2ccf Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Fri, 2 Feb 2024 12:30:39 +0800 Subject: [PATCH 0071/1082] fix lint error --- src/components/Modal/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index 43a2c281415a..6692f2751e40 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -62,7 +62,7 @@ type BaseModalProps = Partial & { /** Should we use a custom backdrop for the modal? (This prevents focus issues on desktop) */ shouldUseCustomBackdrop?: boolean; - + /** * Whether the modal should enable the new focus manager. * We are attempting to migrate to a new refocus manager, adding this property for gradual migration. From a73e36e5bf801b65ade15afcbf284a9b7f1f5239 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Sun, 4 Feb 2024 22:20:22 +0100 Subject: [PATCH 0072/1082] export tax rates types in onyx types --- src/types/onyx/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index d0ac2ce395fa..138bc0ba2270 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -44,6 +44,7 @@ import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; import type {PolicyReportField, PolicyReportFields} from './PolicyReportField'; import type {PolicyTag, PolicyTags} from './PolicyTag'; +import type {PolicyTaxRates, TaxRate, TaxRates} from './PolicyTaxRates'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; @@ -121,6 +122,9 @@ export type { PolicyMembers, PolicyTag, PolicyTags, + TaxRate, + TaxRates, + PolicyTaxRates, PrivatePersonalDetails, RecentWaypoint, RecentlyUsedCategories, From 2169530a3d81ec7afec8daea7d12b9b1fb194f57 Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Mon, 5 Feb 2024 10:38:52 +0200 Subject: [PATCH 0073/1082] Corrected Currency Display: Enforce Two Decimal Places in Amounts --- src/libs/CurrencyUtils.ts | 16 +++++++++++++--- src/pages/iou/steps/MoneyRequestAmountForm.js | 6 +++--- tests/unit/CurrencyUtilsTest.js | 17 ++++++++++++++--- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts index 42387e03c80b..32d2e62e8af1 100644 --- a/src/libs/CurrencyUtils.ts +++ b/src/libs/CurrencyUtils.ts @@ -88,10 +88,19 @@ function convertToBackendAmount(amountAsFloat: number): number { * * @note we do not support any currencies with more than two decimal places. */ -function convertToFrontendAmount(amountAsInt: number): number { +function convertToFrontendAmountAsInteger(amountAsInt: number): number { return Math.trunc(amountAsInt) / 100.0; } +/** + * Takes an amount in "cents" as an integer and converts it to a string amount used in the frontend. + * + * @note we do not support any currencies with more than two decimal places. + */ +function convertToFrontendAmountAsString(amountAsInt: number): string { + return convertToFrontendAmountAsInteger(amountAsInt).toFixed(2); +} + /** * Given an amount in the "cents", convert it to a string for display in the UI. * The backend always handle things in "cents" (subunit equal to 1/100) @@ -105,7 +114,7 @@ function convertToDisplayString(amountInCents = 0, currency: string = CONST.CURR return Localize.translateLocal('common.tbd'); } - const convertedAmount = convertToFrontendAmount(amountInCents); + const convertedAmount = convertToFrontendAmountAsInteger(amountInCents); return NumberFormatUtils.format(BaseLocaleListener.getPreferredLocale(), convertedAmount, { style: 'currency', currency, @@ -131,7 +140,8 @@ export { getCurrencySymbol, isCurrencySymbolLTR, convertToBackendAmount, - convertToFrontendAmount, + convertToFrontendAmountAsInteger, + convertToFrontendAmountAsString, convertToDisplayString, isValidCurrencyCode, }; diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.js b/src/pages/iou/steps/MoneyRequestAmountForm.js index 8775562d4476..3d140a9e16b8 100644 --- a/src/pages/iou/steps/MoneyRequestAmountForm.js +++ b/src/pages/iou/steps/MoneyRequestAmountForm.js @@ -68,7 +68,7 @@ const getNewSelection = (oldSelection, prevLength, newLength) => { }; const isAmountInvalid = (amount) => !amount.length || parseFloat(amount) < 0.01; -const isTaxAmountInvalid = (currentAmount, taxAmount, isTaxAmountForm) => isTaxAmountForm && currentAmount > CurrencyUtils.convertToFrontendAmount(taxAmount); +const isTaxAmountInvalid = (currentAmount, taxAmount, isTaxAmountForm) => isTaxAmountForm && currentAmount > CurrencyUtils.convertToFrontendAmountAsInteger(taxAmount); const AMOUNT_VIEW_ID = 'amountView'; const NUM_PAD_CONTAINER_VIEW_ID = 'numPadContainerView'; @@ -83,7 +83,7 @@ function MoneyRequestAmountForm({amount, taxAmount, currency, isEditing, forward const isTaxAmountForm = Navigation.getActiveRoute().includes('taxAmount'); const decimals = CurrencyUtils.getCurrencyDecimals(currency); - const selectedAmountAsString = amount ? CurrencyUtils.convertToFrontendAmount(amount).toString() : ''; + const selectedAmountAsString = amount ? CurrencyUtils.convertToFrontendAmountAsString(amount) : ''; const [currentAmount, setCurrentAmount] = useState(selectedAmountAsString); const [formError, setFormError] = useState(''); @@ -119,7 +119,7 @@ function MoneyRequestAmountForm({amount, taxAmount, currency, isEditing, forward }; const initializeAmount = useCallback((newAmount) => { - const frontendAmount = newAmount ? CurrencyUtils.convertToFrontendAmount(newAmount).toString() : ''; + const frontendAmount = newAmount ? CurrencyUtils.convertToFrontendAmountAsString(newAmount) : ''; setCurrentAmount(frontendAmount); setSelection({ start: frontendAmount.length, diff --git a/tests/unit/CurrencyUtilsTest.js b/tests/unit/CurrencyUtilsTest.js index 89e1e2ffb3be..c2b0a294c467 100644 --- a/tests/unit/CurrencyUtilsTest.js +++ b/tests/unit/CurrencyUtilsTest.js @@ -106,18 +106,29 @@ describe('CurrencyUtils', () => { }); }); - describe('convertToFrontendAmount', () => { + describe('convertToFrontendAmountAsInteger', () => { test.each([ [2500, 25], [2550, 25.5], [25, 0.25], [2500, 25], [2500.5, 25], // The backend should never send a decimal .5 value - ])('Correctly converts %s to amount in units handled in frontend', (amount, expectedResult) => { - expect(CurrencyUtils.convertToFrontendAmount(amount)).toBe(expectedResult); + ])('Correctly converts %s to amount in units handled in frontend as an integer', (amount, expectedResult) => { + expect(CurrencyUtils.convertToFrontendAmountAsInteger(amount)).toBe(expectedResult); }); }); + describe('convertToFrontendAmountAsString', () => { + test.each([ + [2500, '25.00'], + [2550, '25.50'], + [25, '0.25'], + [2500, '25.00'], + [2500.5, '25.00'], + ])('Correctly converts %s to amount in units handled in frontend as a string', (amount, expectedResult) => { + expect(CurrencyUtils.convertToFrontendAmountAsString(amount)).toBe(expectedResult); + }); + }); describe('convertToDisplayString', () => { test.each([ [CONST.CURRENCY.USD, 25, '$0.25'], From 3e19f56714ba0927c1d0b2cbbb4de72587b926a5 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Mon, 5 Feb 2024 09:52:14 +0100 Subject: [PATCH 0074/1082] add PolicyTaxRates to ONYX KEYS --- src/ONYXKEYS.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 7328fb2543ad..bf1cdacea8d9 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -460,6 +460,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.POLICY_DRAFTS]: OnyxTypes.Policy; [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategories; [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTags; + [ONYXKEYS.COLLECTION.POLICY_TAX_RATE]: OnyxTypes.PolicyTaxRates; [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMembers; [ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; From ec6cf4f93decac331add63ba0f631f76c384d533 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Mon, 5 Feb 2024 09:52:35 +0100 Subject: [PATCH 0075/1082] fix typescript --- .../ReportActionItem/MoneyRequestView.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index e92821f8e94c..9c1db3d07a7a 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -56,7 +56,7 @@ type MoneyRequestViewOnyxPropsWithoutTransaction = { /** Collection of tags attached to a policy */ policyTags: OnyxEntry; - /** Collection of tax rates attached to a policy */ + /** Collection of tax rates attached to a policy */ policyTaxRates: OnyxEntry; /** The expense report or iou report (only will have a value if this is a transaction thread) */ @@ -124,7 +124,8 @@ function MoneyRequestView({ const formattedTaxAmount = transactionTaxAmount ? CurrencyUtils.convertToDisplayString(transactionTaxAmount, transactionCurrency) : ''; - const taxRateTitle = TransactionUtils.getTaxName(policyTaxRates.taxes, transactionTaxCode); + const policyTaxRatesDescription = (policyTaxRates && policyTaxRates.name) ?? ''; + const taxRateTitle = (transactionTaxCode && policyTaxRates && TransactionUtils.getTaxName(policyTaxRates.taxes, transactionTaxCode)) ?? ''; // Flags for allowing or disallowing editing a money request const isSettled = ReportUtils.isSettled(moneyRequestReport?.reportID); @@ -155,8 +156,8 @@ function MoneyRequestView({ const shouldShowTag = isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledOptions(Object.values(policyTagsList))); const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true)); - // A flag for showing tax rate - const shouldShowTax = isTaxPolicyEnabled(isPolicyExpenseChat, policy) && transactionTaxCode && transactionTaxAmount; + // A flag for showing tax rate + const shouldShowTax = isTaxPolicyEnabled(isPolicyExpenseChat, policy) && transactionTaxCode && transactionTaxAmount; const {getViolationsForField} = useViolations(transactionViolations ?? []); const hasViolations = useCallback((field: ViolationField): boolean => !!canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]); @@ -370,7 +371,7 @@ function MoneyRequestView({ Date: Mon, 5 Feb 2024 10:07:04 +0100 Subject: [PATCH 0076/1082] use optional chaining --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 9fdcd7b5125a..2163332b507e 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -124,7 +124,7 @@ function MoneyRequestView({ const formattedTaxAmount = transactionTaxAmount ? CurrencyUtils.convertToDisplayString(transactionTaxAmount, transactionCurrency) : ''; - const policyTaxRatesDescription = (policyTaxRates && policyTaxRates.name) ?? ''; + const policyTaxRatesDescription = policyTaxRates?.name; const taxRateTitle = (transactionTaxCode && policyTaxRates && TransactionUtils.getTaxName(policyTaxRates.taxes, transactionTaxCode)) ?? ''; // Flags for allowing or disallowing editing a money request From 72d282fba6972d757af95451b3f19f40f47a297b Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Tue, 6 Feb 2024 22:49:57 +0200 Subject: [PATCH 0077/1082] fix the case where there is no decimal value passed --- src/libs/CurrencyUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts index 32d2e62e8af1..9e8475b0efe0 100644 --- a/src/libs/CurrencyUtils.ts +++ b/src/libs/CurrencyUtils.ts @@ -98,7 +98,8 @@ function convertToFrontendAmountAsInteger(amountAsInt: number): number { * @note we do not support any currencies with more than two decimal places. */ function convertToFrontendAmountAsString(amountAsInt: number): string { - return convertToFrontendAmountAsInteger(amountAsInt).toFixed(2); + const shouldShowDecimal = amountAsInt % 100 === 0; + return amountAsInt ? convertToFrontendAmountAsInteger(amountAsInt).toFixed(shouldShowDecimal ? 0 : 2) : ''; } /** From 8e80ffbf002b2198fc43f5c9f244515cd6ba62ec Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Tue, 6 Feb 2024 22:50:45 +0200 Subject: [PATCH 0078/1082] cleaning --- src/pages/iou/steps/MoneyRequestAmountForm.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.js b/src/pages/iou/steps/MoneyRequestAmountForm.js index 3d140a9e16b8..aec204772bc5 100644 --- a/src/pages/iou/steps/MoneyRequestAmountForm.js +++ b/src/pages/iou/steps/MoneyRequestAmountForm.js @@ -83,8 +83,7 @@ function MoneyRequestAmountForm({amount, taxAmount, currency, isEditing, forward const isTaxAmountForm = Navigation.getActiveRoute().includes('taxAmount'); const decimals = CurrencyUtils.getCurrencyDecimals(currency); - const selectedAmountAsString = amount ? CurrencyUtils.convertToFrontendAmountAsString(amount) : ''; - + const selectedAmountAsString = CurrencyUtils.convertToFrontendAmountAsString(amount); const [currentAmount, setCurrentAmount] = useState(selectedAmountAsString); const [formError, setFormError] = useState(''); const [shouldUpdateSelection, setShouldUpdateSelection] = useState(true); @@ -119,7 +118,7 @@ function MoneyRequestAmountForm({amount, taxAmount, currency, isEditing, forward }; const initializeAmount = useCallback((newAmount) => { - const frontendAmount = newAmount ? CurrencyUtils.convertToFrontendAmountAsString(newAmount) : ''; + const frontendAmount = CurrencyUtils.convertToFrontendAmountAsString(newAmount); setCurrentAmount(frontendAmount); setSelection({ start: frontendAmount.length, From 8315492b481df524ab720d2751ba9dfe8e241d29 Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Tue, 6 Feb 2024 22:57:07 +0200 Subject: [PATCH 0079/1082] fixing tests --- tests/unit/CurrencyUtilsTest.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/CurrencyUtilsTest.js b/tests/unit/CurrencyUtilsTest.js index c2b0a294c467..2178f029240c 100644 --- a/tests/unit/CurrencyUtilsTest.js +++ b/tests/unit/CurrencyUtilsTest.js @@ -120,10 +120,9 @@ describe('CurrencyUtils', () => { describe('convertToFrontendAmountAsString', () => { test.each([ - [2500, '25.00'], + [2500, '25'], [2550, '25.50'], [25, '0.25'], - [2500, '25.00'], [2500.5, '25.00'], ])('Correctly converts %s to amount in units handled in frontend as a string', (amount, expectedResult) => { expect(CurrencyUtils.convertToFrontendAmountAsString(amount)).toBe(expectedResult); From b3dbe2607ec04fb9b8519fc3f1ed828e0857fa96 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 7 Feb 2024 09:28:57 +0100 Subject: [PATCH 0080/1082] add tax amount API types --- src/libs/API/types.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index a4ab3db9a7cd..f6ec3b8732e3 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -121,6 +121,8 @@ const WRITE_COMMANDS = { UPDATE_MONEY_REQUEST_BILLABLE: 'UpdateMoneyRequestBillable', UPDATE_MONEY_REQUEST_MERCHANT: 'UpdateMoneyRequestMerchant', UPDATE_MONEY_REQUEST_TAG: 'UpdateMoneyRequestTag', + UPDATE_MONEY_REQUEST_TAX_AMOUNT: 'UpdateMoneyRequestTaxAmount', + UPDATE_MONEY_REQUEST_TAX_RATE: 'UpdateMoneyRequestTaxRate', UPDATE_MONEY_REQUEST_DISTANCE: 'UpdateMoneyRequestDistance', UPDATE_MONEY_REQUEST_CATEGORY: 'UpdateMoneyRequestCategory', UPDATE_MONEY_REQUEST_DESCRIPTION: 'UpdateMoneyRequestDescription', @@ -259,6 +261,8 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_MERCHANT]: Parameters.UpdateMoneyRequestParams; [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_BILLABLE]: Parameters.UpdateMoneyRequestParams; [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_TAG]: Parameters.UpdateMoneyRequestParams; + [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_TAX_AMOUNT]: Parameters.UpdateMoneyRequestParams; + [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_TAX_RATE]: Parameters.UpdateMoneyRequestParams; [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DISTANCE]: Parameters.UpdateMoneyRequestParams; [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_CATEGORY]: Parameters.UpdateMoneyRequestParams; [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DESCRIPTION]: Parameters.UpdateMoneyRequestParams; From 0bdef874da54f3a6e8a0630f640e95d08d00928b Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 7 Feb 2024 09:29:48 +0100 Subject: [PATCH 0081/1082] update tax request types --- src/libs/actions/IOU.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index bc1a8b9dd4cf..319889f54496 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1217,18 +1217,18 @@ function updateMoneyRequestTag(transactionID: string, transactionThreadReportID: } /** Updates the created tax amount of a money request */ -function updateMoneyRequestTaxAmount(transactionID: string, optimisticReportActionID: string, val: number) { +function updateMoneyRequestTaxAmount(transactionID: string, optimisticReportActionID: string, taxAmount: number) { const transactionChanges = { - taxAmount: val, + taxAmount, }; const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, optimisticReportActionID, transactionChanges, true); API.write('UpdateMoneyRequestTaxAmount', params, onyxData); } /** Updates the created tax rate of a money request */ -function updateMoneyRequestTaxRate(transactionID: string, optimisticReportActionID: string, val: string) { +function updateMoneyRequestTaxRate(transactionID: string, optimisticReportActionID: string, taxCode: string) { const transactionChanges = { - taxCode: val, + taxCode, }; const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, optimisticReportActionID, transactionChanges, true); API.write('UpdateMoneyRequestTaxRate', params, onyxData); From 3293311d92215d039c17f6654d8718ee94e5d46f Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 7 Feb 2024 11:37:57 +0100 Subject: [PATCH 0082/1082] add taxCode and taxAmount to getUpdatedTransaction --- src/libs/TransactionUtils.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index bce784819769..7964e8c25b78 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -204,6 +204,16 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra shouldStopSmartscan = true; } + if (Object.hasOwn(transactionChanges, 'taxAmount') && typeof transactionChanges.taxAmount === 'number') { + updatedTransaction.taxAmount = isFromExpenseReport ? -transactionChanges.taxAmount : transactionChanges.taxAmount; + shouldStopSmartscan = true; + } + + if (Object.hasOwn(transactionChanges, 'taxCode') && typeof transactionChanges.taxCode === 'string') { + updatedTransaction.taxCode = transactionChanges.taxCode; + shouldStopSmartscan = true; + } + if (Object.hasOwn(transactionChanges, 'billable') && typeof transactionChanges.billable === 'boolean') { updatedTransaction.billable = transactionChanges.billable; } @@ -237,6 +247,8 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra ...(Object.hasOwn(transactionChanges, 'billable') && {billable: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), ...(Object.hasOwn(transactionChanges, 'category') && {category: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), ...(Object.hasOwn(transactionChanges, 'tag') && {tag: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), + ...(Object.hasOwn(transactionChanges, 'taxAmount') && {taxAmount: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), + ...(Object.hasOwn(transactionChanges, 'taxCode') && {taxCode: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), }; return updatedTransaction; From 73d9a10a58b9fc5cda0dd6479f4acf635721f965 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 7 Feb 2024 20:48:26 +0500 Subject: [PATCH 0083/1082] feat: add delete option to deleteable report fields --- .../API/parameters/DeleteReportFieldParams.ts | 6 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/actions/Report.ts | 58 +++++++++++++++++++ src/pages/EditReportFieldDatePage.tsx | 11 +++- src/pages/EditReportFieldDropdownPage.tsx | 11 +++- src/pages/EditReportFieldPage.tsx | 20 +++++++ src/pages/EditReportFieldTextPage.tsx | 11 +++- 8 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 src/libs/API/parameters/DeleteReportFieldParams.ts diff --git a/src/libs/API/parameters/DeleteReportFieldParams.ts b/src/libs/API/parameters/DeleteReportFieldParams.ts new file mode 100644 index 000000000000..393c21af0088 --- /dev/null +++ b/src/libs/API/parameters/DeleteReportFieldParams.ts @@ -0,0 +1,6 @@ +type DeleteReportFieldParams = { + reportID: string; + reportFields: string; +}; + +export default DeleteReportFieldParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index b7c3dff7c342..dba006979dec 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -125,3 +125,4 @@ export type {default as CompleteEngagementModalParams} from './CompleteEngagemen export type {default as SetNameValuePairParams} from './SetNameValuePairParams'; export type {default as SetReportFieldParams} from './SetReportFieldParams'; export type {default as SetReportNameParams} from './SetReportNameParams'; +export type {default as DeleteReportFieldParams} from './DeleteReportFieldParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index c011fa395f0f..5c3581a1a6ac 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -115,6 +115,7 @@ const WRITE_COMMANDS = { COMPLETE_ENGAGEMENT_MODAL: 'CompleteEngagementModal', SET_NAME_VALUE_PAIR: 'SetNameValuePair', SET_REPORT_FIELD: 'Report_SetFields', + DELETE_REPORT_FIELD: 'DELETE_ReportFields', SET_REPORT_NAME: 'RenameReport', } as const; @@ -229,6 +230,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_NAME_VALUE_PAIR]: Parameters.SetNameValuePairParams; [WRITE_COMMANDS.SET_REPORT_FIELD]: Parameters.SetReportFieldParams; [WRITE_COMMANDS.SET_REPORT_NAME]: Parameters.SetReportNameParams; + [WRITE_COMMANDS.DELETE_REPORT_FIELD]: Parameters.DeleteReportFieldParams; }; const READ_COMMANDS = { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 4bff826ceb3a..d8e59232688d 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1614,6 +1614,63 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre API.write(WRITE_COMMANDS.SET_REPORT_FIELD, parameters, {optimisticData, failureData, successData}); } +function deleteReportField(reportID: string, reportField: PolicyReportField) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + reportFields: { + [reportField.fieldID]: null, + }, + pendingFields: { + [reportField.fieldID]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + reportFields: { + [reportField.fieldID]: reportField, + }, + pendingFields: { + [reportField.fieldID]: null, + }, + errorFields: { + [reportField.fieldID]: ErrorUtils.getMicroSecondOnyxError('report.genericUpdateReportFieldFailureMessage'), + }, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + pendingFields: { + [reportField.fieldID]: null, + }, + errorFields: { + [reportField.fieldID]: null, + }, + }, + }, + ]; + + const parameters = { + reportID, + reportFields: JSON.stringify({[reportField.fieldID]: reportField}), + }; + + API.write(WRITE_COMMANDS.DELETE_REPORT_FIELD, parameters, {optimisticData, failureData, successData}); +} + function updateWelcomeMessage(reportID: string, previousValue: string, newValue: string) { // No change needed, navigate back if (previousValue === newValue) { @@ -2884,5 +2941,6 @@ export { clearNewRoomFormError, updateReportField, updateReportName, + deleteReportField, resolveActionableMentionWhisper, }; diff --git a/src/pages/EditReportFieldDatePage.tsx b/src/pages/EditReportFieldDatePage.tsx index 82659eca62c2..3379f6e5f4c1 100644 --- a/src/pages/EditReportFieldDatePage.tsx +++ b/src/pages/EditReportFieldDatePage.tsx @@ -5,6 +5,7 @@ import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {OnyxFormValuesFields} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {ThreeDotsMenuItem} from '@components/HeaderWithBackButton/types'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; @@ -26,11 +27,14 @@ type EditReportFieldDatePageProps = { /** Flag to indicate if the field can be left blank */ isRequired: boolean; + /** Three dot menu item options */ + menuItems?: ThreeDotsMenuItem[]; + /** Callback to fire when the Save button is pressed */ onSubmit: (form: OnyxFormValuesFields) => void; }; -function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, fieldID}: EditReportFieldDatePageProps) { +function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, menuItems, fieldID}: EditReportFieldDatePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const inputRef = useRef(null); @@ -55,7 +59,10 @@ function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, f }} testID={EditReportFieldDatePage.displayName} > - + ) => void; }; @@ -37,7 +41,7 @@ type EditReportFieldDropdownPageOnyxProps = { type EditReportFieldDropdownPageProps = EditReportFieldDropdownPageComponentProps & EditReportFieldDropdownPageOnyxProps; -function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, fieldOptions, recentlyUsedReportFields}: EditReportFieldDropdownPageProps) { +function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, fieldOptions, recentlyUsedReportFields, menuItems}: EditReportFieldDropdownPageProps) { const [searchValue, setSearchValue] = useState(''); const styles = useThemeStyles(); const {getSafeAreaMargins} = useStyleUtils(); @@ -80,7 +84,10 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, > {({insets}) => ( <> - + { + ReportActions.deleteReportField(report.reportID, reportField); + Navigation.dismissModal(report?.reportID); + }; + const fieldValue = isReportFieldTitle ? report.reportName ?? '' : reportField.value ?? reportField.defaultValue; + const menuItems: ThreeDotsMenuItem[] = []; + + const isReportFieldDeletable = report.reportFields?.deletable; + + if (isReportFieldDeletable) { + menuItems.push({icon: Expensicons.Trashcan, text: translate('common.delete'), onSelected: () => handleReportFieldDelete()}); + } + if (reportField.type === 'text' || isReportFieldTitle) { return ( ); @@ -96,6 +114,7 @@ function EditReportFieldPage({route, policy, report, policyReportFields}: EditRe fieldID={reportField.fieldID} fieldValue={fieldValue} isRequired={!reportField.deletable} + menuItems={menuItems} onSubmit={handleReportFieldChange} /> ); @@ -109,6 +128,7 @@ function EditReportFieldPage({route, policy, report, policyReportFields}: EditRe fieldName={Str.UCFirst(reportField.name)} fieldValue={fieldValue} fieldOptions={reportField.values} + menuItems={menuItems} onSubmit={handleReportFieldChange} /> ); diff --git a/src/pages/EditReportFieldTextPage.tsx b/src/pages/EditReportFieldTextPage.tsx index ea9d2d3bed6d..f06ad32e1598 100644 --- a/src/pages/EditReportFieldTextPage.tsx +++ b/src/pages/EditReportFieldTextPage.tsx @@ -4,6 +4,7 @@ import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {OnyxFormValuesFields} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {ThreeDotsMenuItem} from '@components/HeaderWithBackButton/types'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; @@ -26,11 +27,14 @@ type EditReportFieldTextPageProps = { /** Flag to indicate if the field can be left blank */ isRequired: boolean; + /** Three dot menu item options */ + menuItems?: ThreeDotsMenuItem[]; + /** Callback to fire when the Save button is pressed */ onSubmit: (form: OnyxFormValuesFields) => void; }; -function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, fieldID}: EditReportFieldTextPageProps) { +function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, fieldID, menuItems}: EditReportFieldTextPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const inputRef = useRef(null); @@ -55,7 +59,10 @@ function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, f }} testID={EditReportFieldTextPage.displayName} > - + Date: Wed, 7 Feb 2024 17:31:32 +0100 Subject: [PATCH 0084/1082] update Tax Amount Menu Item Description in ConfirmationList --- src/components/MoneyRequestConfirmationList.js | 2 +- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 596811086ee4..824652d481cf 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -810,7 +810,7 @@ function MoneyRequestConfirmationList(props) { diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index cd1874197063..902139610489 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -865,7 +865,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ From 59768825ac3e1a1f07253269c180b5417ed9ef1b Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 7 Feb 2024 17:32:02 +0100 Subject: [PATCH 0085/1082] update Tax Amount Menu Item Description in MoneyRequestView --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index a545bd99c378..7e04b3224119 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -390,7 +390,7 @@ function MoneyRequestView({ Date: Wed, 7 Feb 2024 17:34:15 +0100 Subject: [PATCH 0086/1082] add pending action for taxCode --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 7e04b3224119..927dff901158 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -374,7 +374,7 @@ function MoneyRequestView({ )} {shouldShowTax && ( - + Date: Wed, 7 Feb 2024 17:35:02 +0100 Subject: [PATCH 0087/1082] add pending action for taxAmount --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 927dff901158..e6a20977ae15 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -387,7 +387,7 @@ function MoneyRequestView({ )} {shouldShowTax && ( - + Date: Fri, 9 Feb 2024 13:48:09 +0100 Subject: [PATCH 0088/1082] Implement modal for onboarding --- src/NAVIGATORS.ts | 1 + src/ROUTES.ts | 4 ++++ src/SCREENS.ts | 8 +++++++ src/components/Modal/BaseModal.tsx | 2 +- .../PurposeForUsingExpensifyModal.tsx | 5 +++- src/components/WorkspaceSwitcherButton.tsx | 2 +- .../Navigation/AppNavigator/AuthScreens.tsx | 13 ++++++++-- .../AppNavigator/ModalStackNavigators.tsx | 6 +++++ .../BottomTabBar.tsx | 1 - .../getRootNavigatorScreenOptions.ts | 9 +++++++ src/libs/Navigation/NavigationRoot.tsx | 2 ++ src/libs/Navigation/linkingConfig/config.ts | 24 +++++++++++++++++++ .../linkingConfig/getAdaptedStateFromPath.ts | 3 +++ src/libs/Navigation/types.ts | 8 +++++++ 14 files changed, 82 insertions(+), 6 deletions(-) diff --git a/src/NAVIGATORS.ts b/src/NAVIGATORS.ts index 3bc9c5e57b9b..10e268b07520 100644 --- a/src/NAVIGATORS.ts +++ b/src/NAVIGATORS.ts @@ -7,5 +7,6 @@ export default { BOTTOM_TAB_NAVIGATOR: 'BottomTabNavigator', LEFT_MODAL_NAVIGATOR: 'LeftModalNavigator', RIGHT_MODAL_NAVIGATOR: 'RightModalNavigator', + ONBOARDING_MODAL_NAVIGATOR: 'OnboardingModalNavigator', FULL_SCREEN_NAVIGATOR: 'FullScreenNavigator', } as const; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index e987c5b94d7d..c1ca60e35af0 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -491,6 +491,10 @@ const ROUTES = { getRoute: (contentType: string) => `referral/${contentType}` as const, }, PROCESS_MONEY_REQUEST_HOLD: 'hold-request-educational', + ONBOARDING_WELCOME: 'onboarding/welcome', + ONBOARDING_PURPOSE: 'onboarding/purpose', + WELCOME: 'welcome', + PURPOSE: 'purpose', } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index e2f0e9745561..02f2536cb602 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -119,6 +119,9 @@ const SCREENS = { REFERRAL: 'Referral', PROCESS_MONEY_REQUEST_HOLD: 'ProcessMoneyRequestHold', }, + ONBOARDING_MODAL: { + ONBOARDING: 'Onboarding', + }, SIGN_IN_WITH_APPLE_DESKTOP: 'AppleSignInDesktop', SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop', DESKTOP_SIGN_IN_REDIRECT: 'DesktopSignInRedirect', @@ -230,6 +233,11 @@ const SCREENS = { EDIT_CURRENCY: 'SplitDetails_Edit_Currency', }, + ONBOARDING: { + WELCOME: 'Onboarding_Welcome', + PURPOSE: 'Onboarding_Purpose', + }, + I_KNOW_A_TEACHER: 'I_Know_A_Teacher', INTRO_SCHOOL_PRINCIPAL: 'Intro_School_Principal', I_AM_A_TEACHER: 'I_Am_A_Teacher', diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index c1f4df8d4c99..c5fe44677476 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -191,7 +191,7 @@ function BaseModal( backdropColor={theme.overlay} backdropOpacity={!shouldUseCustomBackdrop && hideBackdrop ? 0 : variables.overlayOpacity} backdropTransitionOutTiming={0} - hasBackdrop={fullscreen} + hasBackdrop={false} coverScreen={fullscreen} style={modalStyle} deviceHeight={windowHeight} diff --git a/src/components/PurposeForUsingExpensifyModal.tsx b/src/components/PurposeForUsingExpensifyModal.tsx index e65646aeac84..175bb4acb698 100644 --- a/src/components/PurposeForUsingExpensifyModal.tsx +++ b/src/components/PurposeForUsingExpensifyModal.tsx @@ -23,6 +23,7 @@ import type {MenuItemProps} from './MenuItem'; import MenuItemList from './MenuItemList'; import Modal from './Modal'; import Text from './Text'; +import Navigation from '@libs/Navigation/Navigation'; // This is not translated because it is a message coming from concierge, which only supports english const messageCopy = { @@ -92,7 +93,7 @@ function PurposeForUsingExpensifyModal({isLoadingApp = false}: PurposeForUsingEx const styles = useThemeStyles(); const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); const navigation = useNavigation(); - const [isModalOpen, setIsModalOpen] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(true); const theme = useTheme(); useEffect(() => { @@ -110,11 +111,13 @@ function PurposeForUsingExpensifyModal({isLoadingApp = false}: PurposeForUsingEx const closeModal = useCallback(() => { Report.dismissEngagementModal(); setIsModalOpen(false); + Navigation.goBack(); }, []); const completeModalAndClose = useCallback((message: string, choice: ValueOf) => { Report.completeEngagementModal(message, choice); setIsModalOpen(false); + Navigation.goBack(); Report.navigateToConciergeChat(); }, []); diff --git a/src/components/WorkspaceSwitcherButton.tsx b/src/components/WorkspaceSwitcherButton.tsx index b7485fbab7a8..03e91b1f71ad 100644 --- a/src/components/WorkspaceSwitcherButton.tsx +++ b/src/components/WorkspaceSwitcherButton.tsx @@ -44,7 +44,7 @@ function WorkspaceSwitcherButton({activeWorkspaceID, policy}: WorkspaceSwitcherB accessible onPress={() => interceptAnonymousUser(() => { - Navigation.navigate(ROUTES.WORKSPACE_SWITCHER); + Navigation.navigate(ROUTES.WELCOME); }) } > diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 00c96d436496..4f3775abb1a3 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -41,6 +41,7 @@ import CentralPaneNavigator from './Navigators/CentralPaneNavigator'; import FullScreenNavigator from './Navigators/FullScreenNavigator'; import LeftModalNavigator from './Navigators/LeftModalNavigator'; import RightModalNavigator from './Navigators/RightModalNavigator'; +import OnboardingModalNavigator from './Navigators/OnboardingModalNavigator'; type AuthScreensProps = { /** Session of currently logged in user */ @@ -256,14 +257,22 @@ function AuthScreens({session, lastOpenedPublicRoomID, isUsingMemoryOnlyKeys = f unsubscribeChatShortcut(); Session.cleanupSession(); }; - + // Rule disabled because this effect is only for component did mount & will component unmount lifecycle event // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - + return ( + require('../../../pages/ProcessMoneyRequestHoldPage').default as React.ComponentType, }); +const OnboardingModalStackNavigator = createModalStackNavigator({ + [SCREENS.ONBOARDING.WELCOME]: () => require('../../../../src/components/PurposeForUsingExpensifyModal').default as React.ComponentType, + [SCREENS.ONBOARDING.PURPOSE]: () => require('../../../../src/components/PurposeForUsingExpensifyModal').default as React.ComponentType, +}) + export { AccountSettingsModalStackNavigator, AddPersonalBankAccountModalStackNavigator, @@ -317,4 +322,5 @@ export { TaskModalStackNavigator, WalletStatementStackNavigator, ProcessMoneyRequestHoldStackNavigator, + OnboardingModalStackNavigator, }; diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx index 7f7e86b3dafb..ef4df063c8a8 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -86,7 +86,6 @@ function BottomTabBar() { - ); } diff --git a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts index c3a69bbd7ccf..26e5829d82e5 100644 --- a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts +++ b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts @@ -39,6 +39,15 @@ const getRootNavigatorScreenOptions: GetRootNavigatorScreenOptions = (isSmallScr right: 0, }, }, + onboardingModalNavigator: { + headerShown: false, + detachPreviousScreen: false, + animationEnabled: false, + presentation: 'transparentModal', + cardStyle: { + backgroundColor: 'transparent', + }, + }, leftModalNavigator: { ...commonScreenOptions, cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, false, props, SLIDE_LEFT_OUTPUT_RANGE_MULTIPLIER), diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 20c426a74c71..d3c13eda4895 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -118,6 +118,8 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N if (!state) { return; } + Log.info("STATE"); + Log.info(JSON.stringify(state)); const activeWorkspaceID = getPolicyIDFromState(state as NavigationState); // Performance optimization to avoid context consumers to delay first render setTimeout(() => { diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 6f96642953af..2c640b05d9d7 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -80,6 +80,30 @@ const config: LinkingOptions['config'] = { }, }, }, + [NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR]: { + screens: { + // [SCREENS.ONBOARDING_MODAL.ONBOARDING]: { + // screens: { + // [SCREENS.ONBOARDING.WELCOME]: { + // path: ROUTES.ONBOARDING_WELCOME, + // exact: true, + // }, + // [SCREENS.ONBOARDING.PURPOSE]: { + // path: ROUTES.ONBOARDING_PURPOSE, + // exact: true, + // }, + // } + // }, + [SCREENS.ONBOARDING.WELCOME]: { + path: ROUTES.WELCOME, + exact: true, + }, + [SCREENS.ONBOARDING.PURPOSE]: { + path: ROUTES.PURPOSE, + exact: true, + } + } + }, [NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: { screens: { [SCREENS.RIGHT_MODAL.SETTINGS]: { diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts index 06e58282da70..2b702453955c 100644 --- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts +++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts @@ -7,6 +7,7 @@ import type {BottomTabName, CentralPaneName, FullScreenName, NavigationPartialRo import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; +import Log from '@libs/Log'; import CENTRAL_PANE_TO_RHP_MAPPING from './CENTRAL_PANE_TO_RHP_MAPPING'; import config from './config'; import FULL_SCREEN_TO_RHP_MAPPING from './FULL_SCREEN_TO_RHP_MAPPING'; @@ -288,6 +289,8 @@ const getAdaptedStateFromPath: GetAdaptedStateFromPath = (path, options) => { const policyID = isAnonymous ? undefined : extractPolicyIDFromPath(path); const state = getStateFromPath(pathWithoutPolicyID, options) as PartialState>; + Log.info("STATE FROM PATH"); + Log.info(JSON.stringify(state)); replacePathInNestedState(state, path); if (state === undefined) { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index d544c2ffa3b6..c5022bf1d278 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -402,6 +402,12 @@ type FullScreenNavigatorParamList = { [SCREENS.SETTINGS_CENTRAL_PANE]: NavigatorScreenParams; }; +type OnboardingModalNavigatorParamList = { + [SCREENS.ONBOARDING_MODAL.ONBOARDING]: undefined; + [SCREENS.ONBOARDING.WELCOME]: undefined; + [SCREENS.ONBOARDING.PURPOSE]: undefined; +}; + type BottomTabNavigatorParamList = { [SCREENS.HOME]: undefined; [SCREENS.ALL_SETTINGS]: undefined; @@ -461,6 +467,7 @@ type AuthScreensParamList = { [NAVIGATORS.LEFT_MODAL_NAVIGATOR]: NavigatorScreenParams; [NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: NavigatorScreenParams; [NAVIGATORS.FULL_SCREEN_NAVIGATOR]: NavigatorScreenParams; + [NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR]: NavigatorScreenParams; [SCREENS.DESKTOP_SIGN_IN_REDIRECT]: undefined; }; @@ -494,6 +501,7 @@ export type { BottomTabNavigatorParamList, LeftModalNavigatorParamList, RightModalNavigatorParamList, + OnboardingModalNavigatorParamList, PublicScreensParamList, MoneyRequestNavigatorParamList, SplitDetailsNavigatorParamList, From cf53a8ab1e05158afeac26c335fbb9e2a44ac32a Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Fri, 9 Feb 2024 13:48:33 +0100 Subject: [PATCH 0089/1082] Add onboarding navigator --- .../Navigators/OnboardingModalNavigator.tsx | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx diff --git a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx new file mode 100644 index 000000000000..8d6a7a931f85 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx @@ -0,0 +1,45 @@ +import React, { useMemo } from 'react'; +import {View} from 'react-native'; +import NoDropZone from "@components/DragAndDrop/NoDropZone"; +import type { OnboardingModalNavigatorParamList } from "@libs/Navigation/types"; +import { createStackNavigator } from "@react-navigation/stack"; +import SCREENS from "@src/SCREENS"; +import ModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/ModalNavigatorScreenOptions'; +import useThemeStyles from '@hooks/useThemeStyles'; +// import * as ModalStackNavigators from '@libs/Navigation/AppNavigator/ModalStackNavigators'; +import PurposeForUsingExpensifyModal from '@components/PurposeForUsingExpensifyModal'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import Overlay from './Overlay'; + +const Stack = createStackNavigator(); + +function OnboardingModalNavigator() { + + const styles = useThemeStyles(); + const screenOptions = useMemo(() => ModalNavigatorScreenOptions(styles), [styles]); + const {isSmallScreenWidth} = useWindowDimensions(); + + return + + {!isSmallScreenWidth && {}}/>} + + {/* */} + + + + + +} + +OnboardingModalNavigator.displayName = 'OnboardingModalNavigator'; + +export default OnboardingModalNavigator; \ No newline at end of file From 30b74113ebdf8935ad38e7d14652cc234a5b9449 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Fri, 9 Feb 2024 15:28:24 +0100 Subject: [PATCH 0090/1082] modify getAdaptedState to handle onboarding navigator --- .../linkingConfig/getAdaptedStateFromPath.ts | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts index 2b702453955c..ea6c94f1b698 100644 --- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts +++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts @@ -2,12 +2,12 @@ import type {NavigationState, PartialState} from '@react-navigation/native'; import {getStateFromPath} from '@react-navigation/native'; import {isAnonymousUser} from '@libs/actions/Session'; import getIsSmallScreenWidth from '@libs/getIsSmallScreenWidth'; +import Log from '@libs/Log'; import getTopmostNestedRHPRoute from '@libs/Navigation/getTopmostNestedRHPRoute'; import type {BottomTabName, CentralPaneName, FullScreenName, NavigationPartialRoute, RootStackParamList} from '@libs/Navigation/types'; import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; -import Log from '@libs/Log'; import CENTRAL_PANE_TO_RHP_MAPPING from './CENTRAL_PANE_TO_RHP_MAPPING'; import config from './config'; import FULL_SCREEN_TO_RHP_MAPPING from './FULL_SCREEN_TO_RHP_MAPPING'; @@ -145,6 +145,8 @@ function getAdaptedState(state: PartialState const fullScreenNavigator = state.routes.find((route) => route.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR); const rhpNavigator = state.routes.find((route) => route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR); const lhpNavigator = state.routes.find((route) => route.name === NAVIGATORS.LEFT_MODAL_NAVIGATOR); + const onboardingModalNavigator = state.routes.find((route) => route.name === NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR); + if (rhpNavigator) { // Routes // - matching bottom tab @@ -177,13 +179,13 @@ function getAdaptedState(state: PartialState metainfo, }; } - if (lhpNavigator) { + if (lhpNavigator ?? onboardingModalNavigator) { // Routes // - default bottom tab // - default central pane on desktop layout - // - found lhp + // - found lhp / onboardingModalNavigator - // Currently there is only the search and workspace switcher in LHP both can have any central pane under the overlay. + // There is no screen in these navigators that would have mandatory central pane, bottom tab or fullscreen navigator. metainfo.isCentralPaneAndBottomTabMandatory = false; metainfo.isFullScreenNavigatorMandatory = false; const routes = []; @@ -202,7 +204,15 @@ function getAdaptedState(state: PartialState }), ); } - routes.push(lhpNavigator); + + // Separate ifs are necessary for typescript to see that we are not pushing unedinfed to the array. + if (lhpNavigator) { + routes.push(lhpNavigator); + } + + if (onboardingModalNavigator) { + routes.push(onboardingModalNavigator); + } return { adaptedState: getRoutesWithIndex(routes), @@ -266,6 +276,10 @@ function getAdaptedState(state: PartialState const matchingCentralPaneRoute = getMatchingCentralPaneRouteForState(state); if (matchingCentralPaneRoute) { routes.push(createCentralPaneNavigator(matchingCentralPaneRoute)); + } else { + // If there is no matching central pane, we want to add the default one. + metainfo.isCentralPaneAndBottomTabMandatory = false; + routes.push(createCentralPaneNavigator({name: SCREENS.REPORT})); } return { @@ -289,14 +303,16 @@ const getAdaptedStateFromPath: GetAdaptedStateFromPath = (path, options) => { const policyID = isAnonymous ? undefined : extractPolicyIDFromPath(path); const state = getStateFromPath(pathWithoutPolicyID, options) as PartialState>; - Log.info("STATE FROM PATH"); + Log.info('STATE FROM PATH'); Log.info(JSON.stringify(state)); replacePathInNestedState(state, path); if (state === undefined) { throw new Error('Unable to parse path'); } - return getAdaptedState(state, policyID); + + const tmp = getAdaptedState(state, policyID); + return tmp; }; export default getAdaptedStateFromPath; From 1895d1841f1da4c1ea2ff498493bf3642e80548f Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Mon, 12 Feb 2024 11:30:27 +0100 Subject: [PATCH 0091/1082] Adjust designs for medium sized screens --- src/CONST.ts | 1 + src/ROUTES.ts | 4 +- src/SCREENS.ts | 2 +- src/components/Modal/BaseModal.tsx | 10 +++- src/components/Modal/types.ts | 3 + src/hooks/useOnboardingLayout.ts | 16 ++++++ src/hooks/useWindowDimensions/index.ts | 3 + .../Navigation/AppNavigator/AuthScreens.tsx | 2 +- .../AppNavigator/ModalStackNavigators.tsx | 4 +- .../Navigators/OnboardingModalNavigator.tsx | 57 ++++++++----------- .../AppNavigator/Navigators/Overlay.tsx | 18 +++++- src/libs/Navigation/NavigationRoot.tsx | 6 +- src/libs/Navigation/linkingConfig/config.ts | 22 ++----- src/libs/Navigation/types.ts | 2 +- src/pages/OnboardingPersonalDetails.tsx | 46 +++++++++++++++ src/pages/OnboardingPurpose.tsx | 48 ++++++++++++++++ src/styles/index.ts | 12 ++++ .../utils/generators/ModalStyleUtils.ts | 35 +++++++++++- 18 files changed, 225 insertions(+), 66 deletions(-) create mode 100644 src/hooks/useOnboardingLayout.ts create mode 100644 src/pages/OnboardingPersonalDetails.tsx create mode 100644 src/pages/OnboardingPurpose.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 6c726cde12f7..f9f4ad1c562f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -715,6 +715,7 @@ const CONST = { BOTTOM_DOCKED: 'bottom_docked', POPOVER: 'popover', RIGHT_DOCKED: 'right_docked', + ONBOARDING: 'onboarding', }, ANCHOR_ORIGIN_VERTICAL: { TOP: 'top', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index c1ca60e35af0..108bd21a8251 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -491,10 +491,8 @@ const ROUTES = { getRoute: (contentType: string) => `referral/${contentType}` as const, }, PROCESS_MONEY_REQUEST_HOLD: 'hold-request-educational', - ONBOARDING_WELCOME: 'onboarding/welcome', + ONBOARDING_PERSONAL_DETAILS: 'onboarding/personal-details', ONBOARDING_PURPOSE: 'onboarding/purpose', - WELCOME: 'welcome', - PURPOSE: 'purpose', } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 02f2536cb602..6f1cf39207c8 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -234,7 +234,7 @@ const SCREENS = { }, ONBOARDING: { - WELCOME: 'Onboarding_Welcome', + PERSONAL_DETAILS: 'Onboarding_Personal_Details', PURPOSE: 'Onboarding_Purpose', }, diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index c5fe44677476..d950478cae6b 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native'; import ReactNativeModal from 'react-native-modal'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; import useKeyboardState from '@hooks/useKeyboardState'; +import useOnboardingLayout from '@hooks/useOnboardingLayout'; import usePrevious from '@hooks/usePrevious'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -22,6 +23,7 @@ function BaseModal( isVisible, onClose, shouldSetModalVisibility = true, + shouldForceHideBackdrop = false, onModalHide = () => {}, type, popoverAnchorPosition = {}, @@ -48,6 +50,7 @@ function BaseModal( const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {windowWidth, windowHeight, isSmallScreenWidth} = useWindowDimensions(); + const {shouldUseNarrowLayout} = useOnboardingLayout(); const keyboardStateContextValue = useKeyboardState(); const safeAreaInsets = useSafeAreaInsets(); @@ -147,8 +150,9 @@ function BaseModal( popoverAnchorPosition, innerContainerStyle, outerStyle, + shouldUseNarrowLayout, ), - [StyleUtils, type, windowWidth, windowHeight, isSmallScreenWidth, popoverAnchorPosition, innerContainerStyle, outerStyle], + [StyleUtils, type, windowWidth, windowHeight, isSmallScreenWidth, popoverAnchorPosition, innerContainerStyle, outerStyle, shouldUseNarrowLayout], ); const { @@ -189,9 +193,9 @@ function BaseModal( swipeDirection={swipeDirection} isVisible={isVisible} backdropColor={theme.overlay} - backdropOpacity={!shouldUseCustomBackdrop && hideBackdrop ? 0 : variables.overlayOpacity} + backdropOpacity={(!shouldUseCustomBackdrop && hideBackdrop) || shouldForceHideBackdrop ? 0 : variables.overlayOpacity} backdropTransitionOutTiming={0} - hasBackdrop={false} + hasBackdrop={fullscreen} coverScreen={fullscreen} style={modalStyle} deviceHeight={windowHeight} diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index a0cdb737d448..f92532f6e27f 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -20,6 +20,9 @@ type BaseModalProps = Partial & { /** Should we announce the Modal visibility changes? */ shouldSetModalVisibility?: boolean; + /** Should we hide backdrop no matter what value is set in modal styles */ + shouldForceHideBackdrop?: boolean; + /** Callback method fired when the user requests to close the modal */ onClose: () => void; diff --git a/src/hooks/useOnboardingLayout.ts b/src/hooks/useOnboardingLayout.ts new file mode 100644 index 000000000000..99c47643d2b5 --- /dev/null +++ b/src/hooks/useOnboardingLayout.ts @@ -0,0 +1,16 @@ +// eslint-disable-next-line no-restricted-imports +import {useWindowDimensions} from 'react-native'; +import variables from '@styles/variables'; + +type OnboardingLayout = { + shouldUseNarrowLayout: boolean; +}; + +/** + * Onboarding layout for medium screen width is narrowed similarly as on web/desktop. + */ +export default function useOnboardingLayout(): OnboardingLayout { + const {width: windowWidth} = useWindowDimensions(); + + return {shouldUseNarrowLayout: windowWidth > variables.mobileResponsiveWidthBreakpoint}; +} diff --git a/src/hooks/useWindowDimensions/index.ts b/src/hooks/useWindowDimensions/index.ts index b0a29e9f901b..fdaa96d747c5 100644 --- a/src/hooks/useWindowDimensions/index.ts +++ b/src/hooks/useWindowDimensions/index.ts @@ -1,5 +1,6 @@ // eslint-disable-next-line no-restricted-imports import {Dimensions, useWindowDimensions} from 'react-native'; +import Log from '@libs/Log'; import variables from '@styles/variables'; import type WindowDimensions from './types'; @@ -15,6 +16,8 @@ export default function (): WindowDimensions { const isMediumScreenWidth = windowWidth > variables.mobileResponsiveWidthBreakpoint && windowWidth <= variables.tabletResponsiveWidthBreakpoint; const isLargeScreenWidth = windowWidth > variables.tabletResponsiveWidthBreakpoint; + Log.info(`WINDOW WIDTH ${windowWidth}`); + return { windowWidth, windowHeight, diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 4f3775abb1a3..cc17fcf07f4f 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -40,8 +40,8 @@ import BottomTabNavigator from './Navigators/BottomTabNavigator'; import CentralPaneNavigator from './Navigators/CentralPaneNavigator'; import FullScreenNavigator from './Navigators/FullScreenNavigator'; import LeftModalNavigator from './Navigators/LeftModalNavigator'; -import RightModalNavigator from './Navigators/RightModalNavigator'; import OnboardingModalNavigator from './Navigators/OnboardingModalNavigator'; +import RightModalNavigator from './Navigators/RightModalNavigator'; type AuthScreensProps = { /** Session of currently logged in user */ diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index b3ea3d28be04..4eb282595f1c 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -289,9 +289,9 @@ const ProcessMoneyRequestHoldStackNavigator = createModalStackNavigator({ }); const OnboardingModalStackNavigator = createModalStackNavigator({ - [SCREENS.ONBOARDING.WELCOME]: () => require('../../../../src/components/PurposeForUsingExpensifyModal').default as React.ComponentType, + [SCREENS.ONBOARDING.PERSONAL_DETAILS]: () => require('../../../../src/components/PurposeForUsingExpensifyModal').default as React.ComponentType, [SCREENS.ONBOARDING.PURPOSE]: () => require('../../../../src/components/PurposeForUsingExpensifyModal').default as React.ComponentType, -}) +}); export { AccountSettingsModalStackNavigator, diff --git a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx index 8d6a7a931f85..9b40f5748f53 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx @@ -1,45 +1,38 @@ -import React, { useMemo } from 'react'; +import {createStackNavigator} from '@react-navigation/stack'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; -import NoDropZone from "@components/DragAndDrop/NoDropZone"; -import type { OnboardingModalNavigatorParamList } from "@libs/Navigation/types"; -import { createStackNavigator } from "@react-navigation/stack"; -import SCREENS from "@src/SCREENS"; -import ModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/ModalNavigatorScreenOptions'; +import NoDropZone from '@components/DragAndDrop/NoDropZone'; import useThemeStyles from '@hooks/useThemeStyles'; -// import * as ModalStackNavigators from '@libs/Navigation/AppNavigator/ModalStackNavigators'; -import PurposeForUsingExpensifyModal from '@components/PurposeForUsingExpensifyModal'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import Overlay from './Overlay'; +import ModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/ModalNavigatorScreenOptions'; +import type {OnboardingModalNavigatorParamList} from '@libs/Navigation/types'; +import OnboardingPersonalDetails from '@pages/OnboardingPersonalDetails'; +import OnboardingPurpose from '@pages/OnboardingPurpose'; +import SCREENS from '@src/SCREENS'; const Stack = createStackNavigator(); function OnboardingModalNavigator() { - const styles = useThemeStyles(); const screenOptions = useMemo(() => ModalNavigatorScreenOptions(styles), [styles]); - const {isSmallScreenWidth} = useWindowDimensions(); - return - - {!isSmallScreenWidth && {}}/>} - - {/* */} - - - - - + return ( + + + + + + + + + ); } OnboardingModalNavigator.displayName = 'OnboardingModalNavigator'; -export default OnboardingModalNavigator; \ No newline at end of file +export default OnboardingModalNavigator; diff --git a/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx b/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx index 5462b6c0ce4e..6917df30ce26 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx @@ -1,14 +1,16 @@ import {useCardAnimation} from '@react-navigation/stack'; -import React from 'react'; +import React, {useMemo} from 'react'; import {Animated, View} from 'react-native'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import useLocalize from '@hooks/useLocalize'; +import useOnboardingLayout from '@hooks/useOnboardingLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import getOperatingSystem from '@libs/getOperatingSystem'; import CONST from '@src/CONST'; type OverlayProps = { /* Callback to close the modal */ - onPress: () => void; + onPress?: () => void; /* Returns whether a modal is displayed on the left side of the screen. By default, the modal is displayed on the right */ isModalOnTheLeft?: boolean; @@ -18,9 +20,19 @@ function Overlay({onPress, isModalOnTheLeft = false}: OverlayProps) { const styles = useThemeStyles(); const {current} = useCardAnimation(); const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useOnboardingLayout(); + + // non-native styling uses fixed positioning not supported on native platforms + const shouldUseNativeStyles = useMemo(() => { + const os = getOperatingSystem(); + if ((os === CONST.OS.ANDROID || os === CONST.OS.IOS || os === CONST.OS.NATIVE) && shouldUseNarrowLayout) { + return true; + } + return false; + }, [shouldUseNarrowLayout]); return ( - + {/* In the latest Electron version buttons can't be both clickable and draggable. That's why we added this workaround. Because of two Pressable components on the desktop app diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index d3c13eda4895..76e9cfa9d650 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -41,6 +41,8 @@ function parseAndLogRoute(state: NavigationState) { return; } + Log.info(`Current state ${JSON.stringify(state)}`); + const currentPath = customGetPathFromState(state, linkingConfig.config); const focusedRoute = findFocusedRoute(state); @@ -74,6 +76,8 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N return undefined; } + return undefined; + const path = initialUrl ? getPathFromURL(initialUrl) : null; // For non-nullable paths we don't want to set initial state @@ -118,7 +122,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N if (!state) { return; } - Log.info("STATE"); + Log.info('STATE'); Log.info(JSON.stringify(state)); const activeWorkspaceID = getPolicyIDFromState(state as NavigationState); // Performance optimization to avoid context consumers to delay first render diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 2c640b05d9d7..094cd20db93f 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -82,27 +82,15 @@ const config: LinkingOptions['config'] = { }, [NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR]: { screens: { - // [SCREENS.ONBOARDING_MODAL.ONBOARDING]: { - // screens: { - // [SCREENS.ONBOARDING.WELCOME]: { - // path: ROUTES.ONBOARDING_WELCOME, - // exact: true, - // }, - // [SCREENS.ONBOARDING.PURPOSE]: { - // path: ROUTES.ONBOARDING_PURPOSE, - // exact: true, - // }, - // } - // }, - [SCREENS.ONBOARDING.WELCOME]: { - path: ROUTES.WELCOME, + [SCREENS.ONBOARDING.PERSONAL_DETAILS]: { + path: ROUTES.ONBOARDING_PERSONAL_DETAILS, exact: true, }, [SCREENS.ONBOARDING.PURPOSE]: { - path: ROUTES.PURPOSE, + path: ROUTES.ONBOARDING_PURPOSE, exact: true, - } - } + }, + }, }, [NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: { screens: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index c5022bf1d278..5f018ccb6561 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -404,7 +404,7 @@ type FullScreenNavigatorParamList = { type OnboardingModalNavigatorParamList = { [SCREENS.ONBOARDING_MODAL.ONBOARDING]: undefined; - [SCREENS.ONBOARDING.WELCOME]: undefined; + [SCREENS.ONBOARDING.PERSONAL_DETAILS]: undefined; [SCREENS.ONBOARDING.PURPOSE]: undefined; }; diff --git a/src/pages/OnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails.tsx new file mode 100644 index 000000000000..370c8d10cbd1 --- /dev/null +++ b/src/pages/OnboardingPersonalDetails.tsx @@ -0,0 +1,46 @@ +import React, {useCallback, useState} from 'react'; +import {View} from 'react-native'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Modal from '@components/Modal'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import Navigation from '@libs/Navigation/Navigation'; +import * as Report from '@userActions/Report'; +import CONST from '@src/CONST'; + +function OnboardingPersonalDetails() { + const styles = useThemeStyles(); + const {windowHeight} = useWindowDimensions(); + const [isModalOpen, setIsModalOpen] = useState(true); + const theme = useTheme(); + + const closeModal = useCallback(() => { + Report.dismissEngagementModal(); + Navigation.goBack(); + setIsModalOpen(false); + }, []); + + return ( + + + + + + ); +} + +OnboardingPersonalDetails.displayName = 'OnboardingPersonalDetails'; +export default OnboardingPersonalDetails; diff --git a/src/pages/OnboardingPurpose.tsx b/src/pages/OnboardingPurpose.tsx new file mode 100644 index 000000000000..9ad9c0d74334 --- /dev/null +++ b/src/pages/OnboardingPurpose.tsx @@ -0,0 +1,48 @@ +import React, {useCallback, useState} from 'react'; +import {View} from 'react-native'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Modal from '@components/Modal'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import Navigation from '@libs/Navigation/Navigation'; +import * as Report from '@userActions/Report'; +import CONST from '@src/CONST'; + +function OnboardingPurpose() { + const styles = useThemeStyles(); + const {windowHeight} = useWindowDimensions(); + const [isModalOpen, setIsModalOpen] = useState(true); + const theme = useTheme(); + + const closeModal = useCallback(() => { + Report.dismissEngagementModal(); + Navigation.goBack(); + setIsModalOpen(false); + }, []); + + return ( + + + + + + ); +} + +OnboardingPurpose.displayName = 'OnboardingPurpose'; +export default OnboardingPurpose; diff --git a/src/styles/index.ts b/src/styles/index.ts index 2e16f1dbd7af..93c7f5d1e1b0 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1711,6 +1711,18 @@ const styles = (theme: ThemeColors) => }), } satisfies ViewStyle), + nativeOverlayStyles: (current: OverlayStylesParams) => + ({ + backgroundColor: theme.overlay, + width: '100%', + height: '100%', + opacity: current.progress.interpolate({ + inputRange: [0, 1], + outputRange: [0, variables.overlayOpacity], + extrapolate: 'clamp', + }), + } satisfies ViewStyle), + appContent: { backgroundColor: theme.appBG, overflow: 'hidden', diff --git a/src/styles/utils/generators/ModalStyleUtils.ts b/src/styles/utils/generators/ModalStyleUtils.ts index 335ef8382941..da93b335709f 100644 --- a/src/styles/utils/generators/ModalStyleUtils.ts +++ b/src/styles/utils/generators/ModalStyleUtils.ts @@ -15,6 +15,10 @@ function getCenteredModalStyles(styles: ThemeStyles, windowWidth: number, isSmal }; } +function getOnboardingModalStyles(styles: ThemeStyles, windowWidth: number, shouldUseNarrowLayout: boolean, isFullScreenWhenSmall = false): ViewStyle { + return shouldUseNarrowLayout ? {width: 500, height: 712} : getCenteredModalStyles(styles, windowWidth, !shouldUseNarrowLayout, isFullScreenWhenSmall); +} + type WindowDimensions = { windowWidth: number; windowHeight: number; @@ -41,11 +45,12 @@ type GetModalStylesStyleUtil = { popoverAnchorPosition?: ViewStyle, innerContainerStyle?: ViewStyle, outerStyle?: ViewStyle, + shouldUseNarrowLayout?: boolean, ) => GetModalStyles; }; const createModalStyleUtils: StyleUtilGenerator = ({theme, styles}) => ({ - getModalStyles: (type, windowDimensions, popoverAnchorPosition = {}, innerContainerStyle = {}, outerStyle = {}): GetModalStyles => { + getModalStyles: (type, windowDimensions, popoverAnchorPosition = {}, innerContainerStyle = {}, outerStyle = {}, shouldUseNarrowLayout = false): GetModalStyles => { const {isSmallScreenWidth, windowWidth} = windowDimensions; let modalStyle: GetModalStyles['modalStyle'] = { @@ -162,7 +167,33 @@ const createModalStyleUtils: StyleUtilGenerator = ({the shouldAddTopSafeAreaMargin = false; shouldAddBottomSafeAreaMargin = false; shouldAddTopSafeAreaPadding = false; - shouldAddBottomSafeAreaPadding = false; + break; + case CONST.MODAL.MODAL_TYPE.ONBOARDING: + // Handles how onboarding modals should be rendered on different screens + modalStyle = { + ...modalStyle, + ...{ + alignItems: 'center', + }, + }; + modalContainerStyle = { + boxShadow: '0px 0px 5px 5px rgba(0, 0, 0, 0.1)', + borderWidth: 0, + flex: !shouldUseNarrowLayout ? 1 : undefined, + marginTop: 0, + marginBottom: 0, + borderRadius: !shouldUseNarrowLayout ? 0 : 16, + overflow: 'hidden', + ...getOnboardingModalStyles(styles, windowWidth, shouldUseNarrowLayout, true), + }; + + // Allow this modal to be dismissed with a swipe down or swipe right + // swipeDirection = ['down', 'right']; + animationIn = 'fadeIn'; + animationOut = 'fadeOut'; + shouldAddTopSafeAreaMargin = false; + shouldAddBottomSafeAreaMargin = false; + shouldAddTopSafeAreaPadding = false; break; case CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED: modalStyle = { From cc0d59f016317092a80d93748a0c9aa2932903ec Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Mon, 12 Feb 2024 11:39:21 +0100 Subject: [PATCH 0092/1082] Refactor code --- src/components/PurposeForUsingExpensifyModal.tsx | 5 +---- src/components/WorkspaceSwitcherButton.tsx | 2 +- src/hooks/useWindowDimensions/index.ts | 3 --- src/libs/Navigation/NavigationRoot.tsx | 6 ------ 4 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/components/PurposeForUsingExpensifyModal.tsx b/src/components/PurposeForUsingExpensifyModal.tsx index 175bb4acb698..e65646aeac84 100644 --- a/src/components/PurposeForUsingExpensifyModal.tsx +++ b/src/components/PurposeForUsingExpensifyModal.tsx @@ -23,7 +23,6 @@ import type {MenuItemProps} from './MenuItem'; import MenuItemList from './MenuItemList'; import Modal from './Modal'; import Text from './Text'; -import Navigation from '@libs/Navigation/Navigation'; // This is not translated because it is a message coming from concierge, which only supports english const messageCopy = { @@ -93,7 +92,7 @@ function PurposeForUsingExpensifyModal({isLoadingApp = false}: PurposeForUsingEx const styles = useThemeStyles(); const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); const navigation = useNavigation(); - const [isModalOpen, setIsModalOpen] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(false); const theme = useTheme(); useEffect(() => { @@ -111,13 +110,11 @@ function PurposeForUsingExpensifyModal({isLoadingApp = false}: PurposeForUsingEx const closeModal = useCallback(() => { Report.dismissEngagementModal(); setIsModalOpen(false); - Navigation.goBack(); }, []); const completeModalAndClose = useCallback((message: string, choice: ValueOf) => { Report.completeEngagementModal(message, choice); setIsModalOpen(false); - Navigation.goBack(); Report.navigateToConciergeChat(); }, []); diff --git a/src/components/WorkspaceSwitcherButton.tsx b/src/components/WorkspaceSwitcherButton.tsx index 03e91b1f71ad..b7485fbab7a8 100644 --- a/src/components/WorkspaceSwitcherButton.tsx +++ b/src/components/WorkspaceSwitcherButton.tsx @@ -44,7 +44,7 @@ function WorkspaceSwitcherButton({activeWorkspaceID, policy}: WorkspaceSwitcherB accessible onPress={() => interceptAnonymousUser(() => { - Navigation.navigate(ROUTES.WELCOME); + Navigation.navigate(ROUTES.WORKSPACE_SWITCHER); }) } > diff --git a/src/hooks/useWindowDimensions/index.ts b/src/hooks/useWindowDimensions/index.ts index fdaa96d747c5..b0a29e9f901b 100644 --- a/src/hooks/useWindowDimensions/index.ts +++ b/src/hooks/useWindowDimensions/index.ts @@ -1,6 +1,5 @@ // eslint-disable-next-line no-restricted-imports import {Dimensions, useWindowDimensions} from 'react-native'; -import Log from '@libs/Log'; import variables from '@styles/variables'; import type WindowDimensions from './types'; @@ -16,8 +15,6 @@ export default function (): WindowDimensions { const isMediumScreenWidth = windowWidth > variables.mobileResponsiveWidthBreakpoint && windowWidth <= variables.tabletResponsiveWidthBreakpoint; const isLargeScreenWidth = windowWidth > variables.tabletResponsiveWidthBreakpoint; - Log.info(`WINDOW WIDTH ${windowWidth}`); - return { windowWidth, windowHeight, diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 76e9cfa9d650..20c426a74c71 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -41,8 +41,6 @@ function parseAndLogRoute(state: NavigationState) { return; } - Log.info(`Current state ${JSON.stringify(state)}`); - const currentPath = customGetPathFromState(state, linkingConfig.config); const focusedRoute = findFocusedRoute(state); @@ -76,8 +74,6 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N return undefined; } - return undefined; - const path = initialUrl ? getPathFromURL(initialUrl) : null; // For non-nullable paths we don't want to set initial state @@ -122,8 +118,6 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N if (!state) { return; } - Log.info('STATE'); - Log.info(JSON.stringify(state)); const activeWorkspaceID = getPolicyIDFromState(state as NavigationState); // Performance optimization to avoid context consumers to delay first render setTimeout(() => { From 279a00e27c86d42ecd7b39c03b415fe6c8a04047 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Mon, 12 Feb 2024 11:57:35 +0100 Subject: [PATCH 0093/1082] Add top margin on small devices --- .../createCustomBottomTabNavigator/BottomTabBar.tsx | 1 - src/styles/utils/generators/ModalStyleUtils.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx index ef4df063c8a8..91c4c65cc0af 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -4,7 +4,6 @@ import {View} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import {PressableWithFeedback} from '@components/Pressable'; -import PurposeForUsingExpensifyModal from '@components/PurposeForUsingExpensifyModal'; import Tooltip from '@components/Tooltip'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/styles/utils/generators/ModalStyleUtils.ts b/src/styles/utils/generators/ModalStyleUtils.ts index da93b335709f..33dbbe6a4b6b 100644 --- a/src/styles/utils/generators/ModalStyleUtils.ts +++ b/src/styles/utils/generators/ModalStyleUtils.ts @@ -191,7 +191,7 @@ const createModalStyleUtils: StyleUtilGenerator = ({the // swipeDirection = ['down', 'right']; animationIn = 'fadeIn'; animationOut = 'fadeOut'; - shouldAddTopSafeAreaMargin = false; + shouldAddTopSafeAreaMargin = !shouldUseNarrowLayout; shouldAddBottomSafeAreaMargin = false; shouldAddTopSafeAreaPadding = false; break; From ddd6c4eb18d69aff6039441cfeda8cb8753e2157 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Mon, 12 Feb 2024 16:01:25 +0100 Subject: [PATCH 0094/1082] add base welcome video modal --- .../OnboardingWelcomeVideoModal.tsx | 61 +++++++++++++++++++ src/languages/en.ts | 7 +++ src/styles/index.ts | 5 ++ src/styles/theme/themes/dark.ts | 1 + src/styles/theme/themes/light.ts | 1 + src/styles/theme/types.ts | 1 + 6 files changed, 76 insertions(+) create mode 100644 src/components/OnboardingWelcomeVideoModal.tsx diff --git a/src/components/OnboardingWelcomeVideoModal.tsx b/src/components/OnboardingWelcomeVideoModal.tsx new file mode 100644 index 000000000000..2dc374b8f78a --- /dev/null +++ b/src/components/OnboardingWelcomeVideoModal.tsx @@ -0,0 +1,61 @@ +import React, {useCallback, useState} from 'react'; +import {View} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import CONST from '@src/CONST'; +import Button from './Button'; +import Lottie from './Lottie'; +import LottieAnimations from './LottieAnimations'; +import Modal from './Modal'; +import Text from './Text'; + +function OnboardingWelcomeVideoModal() { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); + const [isModalOpen, setIsModalOpen] = useState(true); + + const closeModal = useCallback(() => { + setIsModalOpen(false); + }, []); + + return ( + + + + + + ; + + + {translate('onboarding.welcomeVideo.title')} + {translate('onboarding.welcomeVideo.description')} +